diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index c41c1ea6..ca62f3a4 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -48,6 +48,7 @@ set(tag_HDRS toolkit/tfilestream.h toolkit/tmap.h toolkit/tmap.tcc + toolkit/tpropertymap.h mpeg/mpegfile.h mpeg/mpegproperties.h mpeg/mpegheader.h @@ -275,6 +276,7 @@ set(toolkit_SRCS toolkit/tfile.cpp toolkit/tfilestream.cpp toolkit/tdebug.cpp + toolkit/tpropertymap.cpp toolkit/unicode.cpp ) diff --git a/taglib/ape/apefile.cpp b/taglib/ape/apefile.cpp index 2973a476..44f459e9 100644 --- a/taglib/ape/apefile.cpp +++ b/taglib/ape/apefile.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include "apefile.h" @@ -109,6 +110,33 @@ TagLib::Tag *APE::File::tag() const return &d->tag; } +PropertyMap APE::File::properties() const +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +void APE::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasAPE) + d->tag.access(APEIndex, false)->removeUnsupportedProperties(properties); + if(d->hasID3v1) + d->tag.access(ID3v1Index, false)->removeUnsupportedProperties(properties); +} + +PropertyMap APE::File::setProperties(const PropertyMap &properties) +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(APEIndex, true)->setProperties(properties); +} + APE::Properties *APE::File::audioProperties() const { return d->properties; diff --git a/taglib/ape/apefile.h b/taglib/ape/apefile.h index 2f22fdde..0bdbd422 100644 --- a/taglib/ape/apefile.h +++ b/taglib/ape/apefile.h @@ -110,6 +110,25 @@ namespace TagLib { */ virtual TagLib::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only APE + * will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties. Forwards to the actual Tag's + * removeUnsupportedProperties() function. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * As for the export, only one tag is taken into account. If the file + * has no tag at all, APE will be created. + */ + PropertyMap setProperties(const PropertyMap &); /*! * Returns the APE::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/ape/apetag.cpp b/taglib/ape/apetag.cpp index 082fd038..8da91ac3 100644 --- a/taglib/ape/apetag.cpp +++ b/taglib/ape/apetag.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "apetag.h" #include "apefooter.h" @@ -174,6 +175,86 @@ void APE::Tag::setTrack(uint i) addValue("TRACK", String::number(i), true); } +// conversions of tag keys between what we use in PropertyMap and what's usual +// for APE tags +static const uint keyConversionsSize = 5; //usual, APE +static const char *keyConversions[][2] = {{"TRACKNUMBER", "TRACK" }, + {"DATE", "YEAR" }, + {"ALBUMARTIST", "ALBUM ARTIST"}, + {"DISCNUMBER", "DISC" }, + {"REMIXER", "MIXARTIST" }}; + +PropertyMap APE::Tag::properties() const +{ + PropertyMap properties; + ItemListMap::ConstIterator it = itemListMap().begin(); + for(; it != itemListMap().end(); ++it) { + String tagName = PropertyMap::prepareKey(it->first); + // if the item is Binary or Locator, or if the key is an invalid string, + // add to unsupportedData + if(it->second.type() != Item::Text || tagName.isNull()) + properties.unsupportedData().append(it->first); + else { + // Some tags need to be handled specially + for(uint i = 0; i < keyConversionsSize; ++i) + if(tagName == keyConversions[i][1]) + tagName = keyConversions[i][0]; + properties[tagName].append(it->second.toStringList()); + } + } + return properties; +} + +void APE::Tag::removeUnsupportedProperties(const StringList &properties) +{ + StringList::ConstIterator it = properties.begin(); + for(; it != properties.end(); ++it) + removeItem(*it); +} + +PropertyMap APE::Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); // make a local copy that can be modified + + // see comment in properties() + for(uint i = 0; i < keyConversionsSize; ++i) + if(properties.contains(keyConversions[i][0])) { + properties.insert(keyConversions[i][1], properties[keyConversions[i][0]]); + properties.erase(keyConversions[i][0]); + } + + // first check if tags need to be removed completely + StringList toRemove; + ItemListMap::ConstIterator remIt = itemListMap().begin(); + for(; remIt != itemListMap().end(); ++remIt) { + String key = PropertyMap::prepareKey(remIt->first); + // only remove if a) key is valid, b) type is text, c) key not contained in new properties + if(!key.isNull() && remIt->second.type() == APE::Item::Text && !properties.contains(key)) + toRemove.append(remIt->first); + } + + for (StringList::Iterator removeIt = toRemove.begin(); removeIt != toRemove.end(); removeIt++) + removeItem(*removeIt); + + // now sync in the "forward direction" + PropertyMap::ConstIterator it = properties.begin(); + for(; it != properties.end(); ++it) { + const String &tagName = it->first; + if(!(itemListMap().contains(tagName)) || !(itemListMap()[tagName].values() == it->second)) { + if(it->second.size() == 0) + removeItem(tagName); + else { + StringList::ConstIterator valueIt = it->second.begin(); + addValue(tagName, *valueIt, true); + ++valueIt; + for(; valueIt != it->second.end(); ++valueIt) + addValue(tagName, *valueIt, false); + } + } + } + return PropertyMap(); +} + APE::Footer *APE::Tag::footer() const { return &d->footer; diff --git a/taglib/ape/apetag.h b/taglib/ape/apetag.h index 13efd5e0..8520609e 100644 --- a/taglib/ape/apetag.h +++ b/taglib/ape/apetag.h @@ -103,6 +103,30 @@ namespace TagLib { virtual void setYear(uint i); virtual void setTrack(uint i); + /*! + * Implements the unified tag dictionary interface -- export function. + * APE tags are perfectly compatible with the dictionary interface because they + * support both arbitrary tag names and multiple values. Currently only + * APE items of type *Text* are handled by the dictionary interface; all *Binary* + * and *Locator* items will be put into the unsupportedData list and can be + * deleted on request using removeUnsupportedProperties(). The same happens + * to Text items if their key is invalid for PropertyMap (which should actually + * never happen). + * + * The only conversion done by this export function is to rename the APE tags + * TRACK to TRACKNUMBER, YEAR to DATE, and ALBUM ARTIST to ALBUMARTIST, respectively, + * in order to be compliant with the names used in other formats. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified tag dictionary interface -- import function. The same + * comments as for the export function apply. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns a pointer to the tag's footer. */ diff --git a/taglib/flac/flacfile.cpp b/taglib/flac/flacfile.cpp index 5065cd29..3ba096f4 100644 --- a/taglib/flac/flacfile.cpp +++ b/taglib/flac/flacfile.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -138,6 +139,41 @@ TagLib::Tag *FLAC::File::tag() const return &d->tag; } +PropertyMap FLAC::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasXiphComment) + return d->tag.access(XiphIndex, false)->properties(); + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +void FLAC::File::removeUnsupportedProperties(const StringList &unsupported) +{ + if(d->hasXiphComment) + d->tag.access(XiphIndex, false)->removeUnsupportedProperties(unsupported); + if(d->hasID3v2) + d->tag.access(ID3v2Index, false)->removeUnsupportedProperties(unsupported); + if(d->hasID3v1) + d->tag.access(ID3v1Index, false)->removeUnsupportedProperties(unsupported); +} + +PropertyMap FLAC::File::setProperties(const PropertyMap &properties) +{ + if(d->hasXiphComment) + return d->tag.access(XiphIndex, false)->setProperties(properties); + else if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(XiphIndex, true)->setProperties(properties); +} + FLAC::Properties *FLAC::File::audioProperties() const { return d->properties; diff --git a/taglib/flac/flacfile.h b/taglib/flac/flacfile.h index 01466a2d..b2ecce22 100644 --- a/taglib/flac/flacfile.h +++ b/taglib/flac/flacfile.h @@ -29,6 +29,7 @@ #include "taglib_export.h" #include "tfile.h" #include "tlist.h" +#include "tag.h" #include "flacpicture.h" #include "flacproperties.h" @@ -36,7 +37,6 @@ namespace TagLib { class Tag; - namespace ID3v2 { class FrameFactory; class Tag; } namespace ID3v1 { class Tag; } namespace Ogg { class XiphComment; } @@ -118,6 +118,23 @@ namespace TagLib { */ virtual TagLib::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains more than one tag (e.g. XiphComment and ID3v1), + * only the first one (in the order XiphComment, ID3v2, ID3v1) will be + * converted to the PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &); + + /*! + * Implements the unified property interface -- import function. + * As with the export, only one tag is taken into account. If the file + * has no tag at all, a XiphComment will be created. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the FLAC::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/it/itfile.cpp b/taglib/it/itfile.cpp index 5f72d3d9..dc03b60a 100644 --- a/taglib/it/itfile.cpp +++ b/taglib/it/itfile.cpp @@ -23,6 +23,7 @@ #include "itfile.h" #include "tdebug.h" #include "modfileprivate.h" +#include "tpropertymap.h" using namespace TagLib; using namespace IT; @@ -65,6 +66,16 @@ Mod::Tag *IT::File::tag() const return &d->tag; } +PropertyMap IT::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap IT::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + IT::Properties *IT::File::audioProperties() const { return &d->properties; diff --git a/taglib/it/itfile.h b/taglib/it/itfile.h index 73256d32..9c507742 100644 --- a/taglib/it/itfile.h +++ b/taglib/it/itfile.h @@ -60,6 +60,18 @@ namespace TagLib { Mod::Tag *tag() const; + /*! + * Forwards to Mod::Tag::properties(). + * BIC: will be removed once File::toDict() is made virtual + */ + PropertyMap properties() const; + + /*! + * Forwards to Mod::Tag::setProperties(). + * BIC: will be removed once File::setProperties() is made virtual + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the IT::Properties for this file. If no audio properties * were read then this will return a null pointer. @@ -74,6 +86,7 @@ namespace TagLib { */ bool save(); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/mod/modfile.cpp b/taglib/mod/modfile.cpp index f242ea51..25fc8715 100644 --- a/taglib/mod/modfile.cpp +++ b/taglib/mod/modfile.cpp @@ -23,6 +23,7 @@ #include "tstringlist.h" #include "tdebug.h" #include "modfileprivate.h" +#include "tpropertymap.h" using namespace TagLib; using namespace Mod; @@ -70,6 +71,16 @@ Mod::Properties *Mod::File::audioProperties() const return &d->properties; } +PropertyMap Mod::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap Mod::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + bool Mod::File::save() { if(readOnly()) { diff --git a/taglib/mod/modfile.h b/taglib/mod/modfile.h index f66a0ef3..9e79659c 100644 --- a/taglib/mod/modfile.h +++ b/taglib/mod/modfile.h @@ -61,6 +61,17 @@ namespace TagLib { Mod::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); /*! * Returns the Mod::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/mod/modtag.cpp b/taglib/mod/modtag.cpp index 1dabe30e..fe6a374c 100644 --- a/taglib/mod/modtag.cpp +++ b/taglib/mod/modtag.cpp @@ -20,6 +20,8 @@ ***************************************************************************/ #include "modtag.h" +#include "tstringlist.h" +#include "tpropertymap.h" using namespace TagLib; using namespace Mod; @@ -120,3 +122,47 @@ void Mod::Tag::setTrackerName(const String &trackerName) { d->trackerName = trackerName; } + +PropertyMap Mod::Tag::properties() const +{ + PropertyMap properties; + properties["TITLE"] = d->title; + properties["COMMENT"] = d->comment; + if(!(d->trackerName.isNull())) + properties["TRACKERNAME"] = d->trackerName; + return properties; +} + +PropertyMap Mod::Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); + properties.removeEmpty(); + StringList oneValueSet; + if(properties.contains("TITLE")) { + d->title = properties["TITLE"].front(); + oneValueSet.append("TITLE"); + } else + d->title = String::null; + + if(properties.contains("COMMENT")) { + d->comment = properties["COMMENT"].front(); + oneValueSet.append("COMMENT"); + } else + d->comment = String::null; + + if(properties.contains("TRACKERNAME")) { + d->trackerName = properties["TRACKERNAME"].front(); + oneValueSet.append("TRACKERNAME"); + } else + d->trackerName = String::null; + + // for each tag that has been set above, remove the first entry in the corresponding + // value list. The others will be returned as unsupported by this format. + for(StringList::Iterator it = oneValueSet.begin(); it != oneValueSet.end(); ++it) { + if(properties[*it].size() == 1) + properties.erase(*it); + else + properties[*it].erase( properties[*it].begin() ); + } + return properties; +} diff --git a/taglib/mod/modtag.h b/taglib/mod/modtag.h index 253a4666..c1a74b10 100644 --- a/taglib/mod/modtag.h +++ b/taglib/mod/modtag.h @@ -159,6 +159,22 @@ namespace TagLib { */ void setTrackerName(const String &trackerName); + /*! + * Implements the unified property interface -- export function. + * Since the module tag is very limited, the exported map is as well. + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Because of the limitations of the module file tag, any tags besides + * COMMENT, TITLE and, if it is an XM file, TRACKERNAME, will be + * returened. Additionally, if the map contains tags with multiple values, + * all but the first will be contained in the returned map of unsupported + * properties. + */ + PropertyMap setProperties(const PropertyMap &); + private: Tag(const Tag &); Tag &operator=(const Tag &); diff --git a/taglib/mpc/mpcfile.cpp b/taglib/mpc/mpcfile.cpp index 216c1b3b..ca9471ae 100644 --- a/taglib/mpc/mpcfile.cpp +++ b/taglib/mpc/mpcfile.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "mpcfile.h" #include "id3v1tag.h" @@ -113,6 +114,34 @@ TagLib::Tag *MPC::File::tag() const return &d->tag; } +PropertyMap MPC::File::properties() const +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +void MPC::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasAPE) + d->tag.access(APEIndex, false)->removeUnsupportedProperties(properties); + if(d->hasID3v1) + d->tag.access(ID3v1Index, false)->removeUnsupportedProperties(properties); +} + +PropertyMap MPC::File::setProperties(const PropertyMap &properties) +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(APE, true)->setProperties(properties); +} + + MPC::Properties *MPC::File::audioProperties() const { return d->properties; diff --git a/taglib/mpc/mpcfile.h b/taglib/mpc/mpcfile.h index 93471cf1..c906ae67 100644 --- a/taglib/mpc/mpcfile.h +++ b/taglib/mpc/mpcfile.h @@ -28,6 +28,7 @@ #include "taglib_export.h" #include "tfile.h" +#include "tag.h" #include "mpcproperties.h" @@ -107,6 +108,22 @@ namespace TagLib { */ virtual TagLib::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only the APE + * tag will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * As with the export, only one tag is taken into account. If the file + * has no tag at all, an APE tag will be created. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the MPC::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/mpeg/id3v2/frames/commentsframe.cpp b/taglib/mpeg/id3v2/frames/commentsframe.cpp index 377a7ee3..2c6c49f9 100644 --- a/taglib/mpeg/id3v2/frames/commentsframe.cpp +++ b/taglib/mpeg/id3v2/frames/commentsframe.cpp @@ -29,6 +29,7 @@ #include #include "commentsframe.h" +#include "tpropertymap.h" using namespace TagLib; using namespace ID3v2; @@ -109,6 +110,19 @@ void CommentsFrame::setTextEncoding(String::Type encoding) d->textEncoding = encoding; } +PropertyMap CommentsFrame::asProperties() const +{ + String key = PropertyMap::prepareKey(description()); + PropertyMap map; + if(key.isEmpty() || key == "COMMENT") + map.insert("COMMENT", text()); + else if(key.isNull()) + map.unsupportedData().append(L"COMM/" + description()); + else + map.insert("COMMENT:" + key, text()); + return map; +} + CommentsFrame *CommentsFrame::findByDescription(const ID3v2::Tag *tag, const String &d) // static { ID3v2::FrameList comments = tag->frameList("COMM"); diff --git a/taglib/mpeg/id3v2/frames/commentsframe.h b/taglib/mpeg/id3v2/frames/commentsframe.h index def01dcf..f65f6f01 100644 --- a/taglib/mpeg/id3v2/frames/commentsframe.h +++ b/taglib/mpeg/id3v2/frames/commentsframe.h @@ -136,6 +136,17 @@ namespace TagLib { */ void setTextEncoding(String::Type encoding); + /*! + * Parses this frame as PropertyMap with a single key. + * - if description() is empty or "COMMENT", the key will be "COMMENT" + * - if description() is not a valid PropertyMap key, the frame will be + * marked unsupported by an entry "COMM/" in the unsupportedData() + * attribute of the returned map. + * - otherwise, the key will be "COMMENT:" + * - The single value will be the frame's text(). + */ + PropertyMap asProperties() const; + /*! * Comments each have a unique description. This searches for a comment * frame with the decription \a d and returns a pointer to it. If no diff --git a/taglib/mpeg/id3v2/frames/textidentificationframe.cpp b/taglib/mpeg/id3v2/frames/textidentificationframe.cpp index cf724922..a026bca9 100644 --- a/taglib/mpeg/id3v2/frames/textidentificationframe.cpp +++ b/taglib/mpeg/id3v2/frames/textidentificationframe.cpp @@ -25,8 +25,9 @@ #include #include - #include "textidentificationframe.h" +#include "tpropertymap.h" +#include "id3v1genres.h" using namespace TagLib; using namespace ID3v2; @@ -57,6 +58,32 @@ TextIdentificationFrame::TextIdentificationFrame(const ByteVector &data) : setData(data); } +TextIdentificationFrame *TextIdentificationFrame::createTIPLFrame(const PropertyMap &properties) // static +{ + TextIdentificationFrame *frame = new TextIdentificationFrame("TIPL"); + StringList l; + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + l.append(it->first); + l.append(it->second.toString(",")); // comma-separated list of names + } + frame->setText(l); + return frame; +} + +TextIdentificationFrame *TextIdentificationFrame::createTMCLFrame(const PropertyMap &properties) // static +{ + TextIdentificationFrame *frame = new TextIdentificationFrame("TMCL"); + StringList l; + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + if(!it->first.startsWith(instrumentPrefix)) // should not happen + continue; + l.append(it->first.substr(instrumentPrefix.size())); + l.append(it->second.toString(",")); + } + frame->setText(l); + return frame; +} + TextIdentificationFrame::~TextIdentificationFrame() { delete d; @@ -92,6 +119,61 @@ void TextIdentificationFrame::setTextEncoding(String::Type encoding) d->textEncoding = encoding; } +// array of allowed TIPL prefixes and their corresponding key value +static const uint involvedPeopleSize = 5; +static const char* involvedPeople[][2] = { + {"ARRANGER", "ARRANGER"}, + {"ENGINEER", "ENGINEER"}, + {"PRODUCER", "PRODUCER"}, + {"DJ-MIX", "DJMIXER"}, + {"MIX", "MIXER"}, +}; + +const KeyConversionMap &TextIdentificationFrame::involvedPeopleMap() // static +{ + static KeyConversionMap m; + if(m.isEmpty()) + for(uint i = 0; i < involvedPeopleSize; ++i) + m.insert(involvedPeople[i][1], involvedPeople[i][0]); + return m; +} + +PropertyMap TextIdentificationFrame::asProperties() const +{ + if(frameID() == "TIPL") + return makeTIPLProperties(); + if(frameID() == "TMCL") + return makeTMCLProperties(); + PropertyMap map; + String tagName = frameIDToKey(frameID()); + if(tagName.isNull()) { + map.unsupportedData().append(frameID()); + return map; + } + StringList values = fieldList(); + if(tagName == "GENRE") { + // Special case: Support ID3v1-style genre numbers. They are not officially supported in + // ID3v2, however it seems that still a lot of programs use them. + for(StringList::Iterator it = values.begin(); it != values.end(); ++it) { + bool ok = false; + int test = it->toInt(&ok); // test if the genre value is an integer + if(ok) + *it = ID3v1::genre(test); + } + } else if(tagName == "DATE") { + for(StringList::Iterator it = values.begin(); it != values.end(); ++it) { + // ID3v2 specifies ISO8601 timestamps which contain a 'T' as separator between date and time. + // Since this is unusual in other formats, the T is removed. + int tpos = it->find("T"); + if(tpos != -1) + (*it)[tpos] = ' '; + } + } + PropertyMap ret; + ret.insert(tagName, values); + return ret; +} + //////////////////////////////////////////////////////////////////////////////// // TextIdentificationFrame protected members //////////////////////////////////////////////////////////////////////////////// @@ -170,6 +252,55 @@ TextIdentificationFrame::TextIdentificationFrame(const ByteVector &data, Header parseFields(fieldData(data)); } +PropertyMap TextIdentificationFrame::makeTIPLProperties() const +{ + PropertyMap map; + if(fieldList().size() % 2 != 0){ + // according to the ID3 spec, TIPL must contain an even number of entries + map.unsupportedData().append(frameID()); + return map; + } + StringList l = fieldList(); + for(StringList::ConstIterator it = l.begin(); it != l.end(); ++it) { + bool found = false; + for(uint i = 0; i < involvedPeopleSize; ++i) + if(*it == involvedPeople[i][0]) { + map.insert(involvedPeople[i][1], (++it)->split(",")); + found = true; + break; + } + if(!found){ + // invalid involved role -> mark whole frame as unsupported in order to be consisten with writing + map.clear(); + map.unsupportedData().append(frameID()); + return map; + } + } + return map; +} + +PropertyMap TextIdentificationFrame::makeTMCLProperties() const +{ + PropertyMap map; + if(fieldList().size() % 2 != 0){ + // according to the ID3 spec, TMCL must contain an even number of entries + map.unsupportedData().append(frameID()); + return map; + } + StringList l = fieldList(); + for(StringList::ConstIterator it = l.begin(); it != l.end(); ++it) { + String instrument = PropertyMap::prepareKey(*it); + if(instrument.isNull()) { + // instrument is not a valid key -> frame unsupported + map.clear(); + map.unsupportedData().append(frameID()); + return map; + } + map.insert(L"PERFORMER:" + instrument, (++it)->split(",")); + } + return map; +} + //////////////////////////////////////////////////////////////////////////////// // UserTextIdentificationFrame public members //////////////////////////////////////////////////////////////////////////////// @@ -191,6 +322,14 @@ UserTextIdentificationFrame::UserTextIdentificationFrame(const ByteVector &data) checkFields(); } +UserTextIdentificationFrame::UserTextIdentificationFrame(const String &description, const StringList &values, String::Type encoding) : + TextIdentificationFrame("TXXX", encoding), + d(0) +{ + setDescription(description); + setText(values); +} + String UserTextIdentificationFrame::toString() const { return "[" + description() + "] " + fieldList().toString(); @@ -238,6 +377,23 @@ void UserTextIdentificationFrame::setDescription(const String &s) TextIdentificationFrame::setText(l); } +PropertyMap UserTextIdentificationFrame::asProperties() const +{ + String tagName = description(); + + PropertyMap map; + String key = map.prepareKey(tagName); + if(key.isNull()) // this frame's description is not a valid PropertyMap key -> add to unsupported list + map.unsupportedData().append(L"TXXX/" + description()); + else { + StringList v = fieldList(); + for(StringList::ConstIterator it = v.begin(); it != v.end(); ++it) + if(*it != description()) + map.insert(key, *it); + } + return map; +} + UserTextIdentificationFrame *UserTextIdentificationFrame::find( ID3v2::Tag *tag, const String &description) // static { diff --git a/taglib/mpeg/id3v2/frames/textidentificationframe.h b/taglib/mpeg/id3v2/frames/textidentificationframe.h index 418ef970..283f0c72 100644 --- a/taglib/mpeg/id3v2/frames/textidentificationframe.h +++ b/taglib/mpeg/id3v2/frames/textidentificationframe.h @@ -36,6 +36,7 @@ namespace TagLib { namespace ID3v2 { class Tag; + typedef Map KeyConversionMap; //! An ID3v2 text identification frame implementation @@ -123,6 +124,20 @@ namespace TagLib { */ explicit TextIdentificationFrame(const ByteVector &data); + /*! + * This is a special factory method to create a TIPL (involved people list) + * frame from the given \a properties. Will parse key=[list of values] data + * into the TIPL format as specified in the ID3 standard. + */ + static TextIdentificationFrame *createTIPLFrame(const PropertyMap &properties); + + /*! + * This is a special factory method to create a TMCL (musician credits list) + * frame from the given \a properties. Will parse key=[list of values] data + * into the TMCL format as specified in the ID3 standard, where key should be + * of the form instrumentPrefix:instrument. + */ + static TextIdentificationFrame *createTMCLFrame(const PropertyMap &properties); /*! * Destroys this TextIdentificationFrame instance. */ @@ -173,6 +188,14 @@ namespace TagLib { */ StringList fieldList() const; + /*! + * Returns a KeyConversionMap mapping a role as it would be used in a PropertyMap + * to the corresponding key used in a TIPL ID3 frame to describe that role. + */ + static const KeyConversionMap &involvedPeopleMap(); + + PropertyMap asProperties() const; + protected: // Reimplementations. @@ -188,6 +211,16 @@ namespace TagLib { TextIdentificationFrame(const TextIdentificationFrame &); TextIdentificationFrame &operator=(const TextIdentificationFrame &); + /*! + * Parses the special structure of a TIPL frame + * Only the whitelisted roles "ARRANGER", "ENGINEER", "PRODUCER", + * "DJMIXER" (ID3: "DJ-MIX") and "MIXER" (ID3: "MIX") are allowed. + */ + PropertyMap makeTIPLProperties() const; + /*! + * Parses the special structure of a TMCL frame. + */ + PropertyMap makeTMCLProperties() const; class TextIdentificationFramePrivate; TextIdentificationFramePrivate *d; }; @@ -218,6 +251,12 @@ namespace TagLib { */ explicit UserTextIdentificationFrame(const ByteVector &data); + /*! + * Creates a user defined text identification frame with the given \a description + * and \a values. + */ + UserTextIdentificationFrame(const String &description, const StringList &values, String::Type encoding = String::UTF8); + virtual String toString() const; /*! @@ -236,6 +275,21 @@ namespace TagLib { void setText(const String &text); void setText(const StringList &fields); + /*! + * A UserTextIdentificationFrame is parsed into a PropertyMap as follows: + * - the key is the frame's description, uppercased + * - if the description contains '::', only the substring after that + * separator is considered as key (compatibility with exfalso) + * - if the above rules don't yield a valid key (e.g. containing non-ASCII + * characters), the returned map will contain an entry "TXXX/" + * in its unsupportedData() list. + * - The values will be copies of the fieldList(). + * - If the description() appears as value in fieldList(), it will be omitted + * in the value list, in order to be compatible with TagLib which copies + * the description() into the fieldList(). + */ + PropertyMap asProperties() const; + /*! * Searches for the user defined text frame with the description \a description * in \a tag. This returns null if no matching frames were found. diff --git a/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp b/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp index 0a8927e7..9d76164d 100644 --- a/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp +++ b/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.cpp @@ -27,7 +27,9 @@ #include "unsynchronizedlyricsframe.h" #include +#include #include +#include using namespace TagLib; using namespace ID3v2; @@ -111,6 +113,30 @@ void UnsynchronizedLyricsFrame::setTextEncoding(String::Type encoding) d->textEncoding = encoding; } +PropertyMap UnsynchronizedLyricsFrame::asProperties() const +{ + PropertyMap map; + String key = PropertyMap::prepareKey(description()); + if(key.isEmpty() || key.upper() == "LYRICS") + map.insert("LYRICS", text()); + else if(key.isNull()) + map.unsupportedData().append(L"USLT/" + description()); + else + map.insert("LYRICS:" + key, text()); + return map; +} + +UnsynchronizedLyricsFrame *UnsynchronizedLyricsFrame::findByDescription(const ID3v2::Tag *tag, const String &d) // static +{ + ID3v2::FrameList lyrics = tag->frameList("USLT"); + + for(ID3v2::FrameList::ConstIterator it = lyrics.begin(); it != lyrics.end(); ++it){ + UnsynchronizedLyricsFrame *frame = dynamic_cast(*it); + if(frame && frame->description() == d) + return frame; + } + return 0; +} //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h b/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h index 0f8260e4..3af354fc 100644 --- a/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h +++ b/taglib/mpeg/id3v2/frames/unsynchronizedlyricsframe.h @@ -134,6 +134,28 @@ namespace TagLib { */ void setTextEncoding(String::Type encoding); + + /*! Parses this frame as PropertyMap with a single key. + * - if description() is empty or "LYRICS", the key will be "LYRICS" + * - if description() is not a valid PropertyMap key, the frame will be + * marked unsupported by an entry "USLT/" in the unsupportedData() + * attribute of the returned map. + * - otherwise, the key will be "LYRICS:" + * - The single value will be the frame's text(). + * Note that currently the language() field is not supported by the PropertyMap + * interface. + */ + PropertyMap asProperties() const; + + /*! + * LyricsFrames each have a unique description. This searches for a lyrics + * frame with the decription \a d and returns a pointer to it. If no + * frame is found that matches the given description null is returned. + * + * \see description() + */ + static UnsynchronizedLyricsFrame *findByDescription(const Tag *tag, const String &d); + protected: // Reimplementations. diff --git a/taglib/mpeg/id3v2/frames/urllinkframe.cpp b/taglib/mpeg/id3v2/frames/urllinkframe.cpp index 09edec40..5e4f2db7 100644 --- a/taglib/mpeg/id3v2/frames/urllinkframe.cpp +++ b/taglib/mpeg/id3v2/frames/urllinkframe.cpp @@ -26,8 +26,10 @@ ***************************************************************************/ #include "urllinkframe.h" +#include "id3v2tag.h" #include #include +#include using namespace TagLib; using namespace ID3v2; @@ -78,6 +80,18 @@ String UrlLinkFrame::toString() const return url(); } +PropertyMap UrlLinkFrame::asProperties() const +{ + String key = frameIDToKey(frameID()); + PropertyMap map; + if(key.isNull()) + // unknown W*** frame - this normally shouldn't happen + map.unsupportedData().append(frameID()); + else + map.insert(key, url()); + return map; +} + void UrlLinkFrame::parseFields(const ByteVector &data) { d->url = String(data); @@ -139,6 +153,30 @@ void UserUrlLinkFrame::setDescription(const String &s) d->description = s; } +PropertyMap UserUrlLinkFrame::asProperties() const +{ + PropertyMap map; + String key = PropertyMap::prepareKey(description()); + if(key.isEmpty() || key.upper() == "URL") + map.insert("URL", url()); + else if(key.isNull()) + map.unsupportedData().append(L"WXXX/" + description()); + else + map.insert("URL:" + key, url()); + return map; +} + +UserUrlLinkFrame *UserUrlLinkFrame::find(ID3v2::Tag *tag, const String &description) // static +{ + FrameList l = tag->frameList("WXXX"); + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) { + UserUrlLinkFrame *f = dynamic_cast(*it); + if(f && f->description() == description) + return f; + } + return 0; +} + void UserUrlLinkFrame::parseFields(const ByteVector &data) { if(data.size() < 2) { diff --git a/taglib/mpeg/id3v2/frames/urllinkframe.h b/taglib/mpeg/id3v2/frames/urllinkframe.h index f89faad0..7ac966b2 100644 --- a/taglib/mpeg/id3v2/frames/urllinkframe.h +++ b/taglib/mpeg/id3v2/frames/urllinkframe.h @@ -68,6 +68,7 @@ namespace TagLib { virtual void setText(const String &s); virtual String toString() const; + PropertyMap asProperties() const; protected: virtual void parseFields(const ByteVector &data); @@ -150,6 +151,22 @@ namespace TagLib { */ void setDescription(const String &s); + /*! + * Parses the UserUrlLinkFrame as PropertyMap. The description() is taken as key, + * and the URL as single value. + * - if description() is empty, the key will be "URL". + * - otherwise, if description() is not a valid key (e.g. containing non-ASCII + * characters), the returned map will contain an entry "WXXX/" + * in its unsupportedData() list. + */ + PropertyMap asProperties() const; + + /*! + * Searches for the user defined url frame with the description \a description + * in \a tag. This returns null if no matching frames were found. + */ + static UserUrlLinkFrame *find(Tag *tag, const String &description); + protected: virtual void parseFields(const ByteVector &data); virtual ByteVector renderFields() const; diff --git a/taglib/mpeg/id3v2/id3v2frame.cpp b/taglib/mpeg/id3v2/id3v2frame.cpp index 7979cf50..6b45f223 100644 --- a/taglib/mpeg/id3v2/id3v2frame.cpp +++ b/taglib/mpeg/id3v2/id3v2frame.cpp @@ -38,6 +38,12 @@ #include "id3v2frame.h" #include "id3v2synchdata.h" +#include "tpropertymap.h" +#include "frames/textidentificationframe.h" +#include "frames/urllinkframe.h" +#include "frames/unsynchronizedlyricsframe.h" +#include "frames/commentsframe.h" +#include "frames/unknownframe.h" using namespace TagLib; using namespace ID3v2; @@ -95,10 +101,56 @@ ByteVector Frame::textDelimiter(String::Type t) return d; } +const String Frame::instrumentPrefix("PERFORMER:"); +const String Frame::commentPrefix("COMMENT:"); +const String Frame::lyricsPrefix("LYRICS:"); +const String Frame::urlPrefix("URL:"); + //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// +Frame *Frame::createTextualFrame(const String &key, const StringList &values) //static +{ + // check if the key is contained in the key<=>frameID mapping + ByteVector frameID = keyToFrameID(key); + if(!frameID.isNull()) { + if(frameID[0] == 'T'){ // text frame + TextIdentificationFrame *frame = new TextIdentificationFrame(frameID, String::UTF8); + frame->setText(values); + return frame; + } else if(values.size() == 1){ // URL frame (not WXXX); support only one value + UrlLinkFrame* frame = new UrlLinkFrame(frameID); + frame->setUrl(values.front()); + return frame; + } + } + // now we check if it's one of the "special" cases: + // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) + if((key == "LYRICS" || key.startsWith(lyricsPrefix)) && values.size() == 1){ + UnsynchronizedLyricsFrame *frame = new UnsynchronizedLyricsFrame(); + frame->setDescription(key == "LYRICS" ? key : key.substr(lyricsPrefix.size())); + frame->setText(values.front()); + return frame; + } + // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) + if((key == "URL" || key.startsWith(urlPrefix)) && values.size() == 1){ + UserUrlLinkFrame *frame = new UserUrlLinkFrame(String::UTF8); + frame->setDescription(key == "URL" ? key : key.substr(urlPrefix.size())); + frame->setUrl(values.front()); + return frame; + } + // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) + if((key == "COMMENT" || key.startsWith(commentPrefix)) && values.size() == 1){ + CommentsFrame *frame = new CommentsFrame(String::UTF8); + frame->setDescription(key == "COMMENT" ? key : key.substr(commentPrefix.size())); + frame->setText(values.front()); + return frame; + } + // if non of the above cases apply, we use a TXXX frame with the key as description + return new UserTextIdentificationFrame(key, values, String::UTF8); +} + Frame::~Frame() { delete d; @@ -262,6 +314,163 @@ String::Type Frame::checkTextEncoding(const StringList &fields, String::Type enc return checkEncoding(fields, encoding, header()->version()); } +static const uint frameTranslationSize = 51; +static const char *frameTranslation[][2] = { + // Text information frames + { "TALB", "ALBUM"}, + { "TBPM", "BPM" }, + { "TCOM", "COMPOSER" }, + { "TCON", "GENRE" }, + { "TCOP", "COPYRIGHT" }, + { "TDEN", "ENCODINGTIME" }, + { "TDLY", "PLAYLISTDELAY" }, + { "TDOR", "ORIGINALDATE" }, + { "TDRC", "DATE" }, + // { "TRDA", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TDAT", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TYER", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + // { "TIME", "DATE" }, // id3 v2.3, replaced by TDRC in v2.4 + { "TDRL", "RELEASEDATE" }, + { "TDTG", "TAGGINGDATE" }, + { "TENC", "ENCODEDBY" }, + { "TEXT", "LYRICIST" }, + { "TFLT", "FILETYPE" }, + //{ "TIPL", "INVOLVEDPEOPLE" }, handled separately + { "TIT1", "CONTENTGROUP" }, + { "TIT2", "TITLE"}, + { "TIT3", "SUBTITLE" }, + { "TKEY", "INITIALKEY" }, + { "TLAN", "LANGUAGE" }, + { "TLEN", "LENGTH" }, + //{ "TMCL", "MUSICIANCREDITS" }, handled separately + { "TMED", "MEDIATYPE" }, + { "TMOO", "MOOD" }, + { "TOAL", "ORIGINALALBUM" }, + { "TOFN", "ORIGINALFILENAME" }, + { "TOLY", "ORIGINALLYRICIST" }, + { "TOPE", "ORIGINALARTIST" }, + { "TOWN", "OWNER" }, + { "TPE1", "ARTIST"}, + { "TPE2", "ALBUMARTIST" }, // id3's spec says 'PERFORMER', but most programs use 'ALBUMARTIST' + { "TPE3", "CONDUCTOR" }, + { "TPE4", "REMIXER" }, // could also be ARRANGER + { "TPOS", "DISCNUMBER" }, + { "TPRO", "PRODUCEDNOTICE" }, + { "TPUB", "PUBLISHER" }, + { "TRCK", "TRACKNUMBER" }, + { "TRSN", "RADIOSTATION" }, + { "TRSO", "RADIOSTATIONOWNER" }, + { "TSOA", "ALBUMSORT" }, + { "TSOP", "ARTISTSORT" }, + { "TSOT", "TITLESORT" }, + { "TSO2", "ALBUMARTISTSORT" }, // non-standard, used by iTunes + { "TSRC", "ISRC" }, + { "TSSE", "ENCODING" }, + // URL frames + { "WCOP", "COPYRIGHTURL" }, + { "WOAF", "FILEWEBPAGE" }, + { "WOAR", "ARTISTWEBPAGE" }, + { "WOAS", "AUDIOSOURCEWEBPAGE" }, + { "WORS", "RADIOSTATIONWEBPAGE" }, + { "WPAY", "PAYMENTWEBPAGE" }, + { "WPUB", "PUBLISHERWEBPAGE" }, + //{ "WXXX", "URL"}, handled specially + // Other frames + { "COMM", "COMMENT" }, + //{ "USLT", "LYRICS" }, handled specially +}; + +Map &idMap() +{ + static Map m; + if(m.isEmpty()) + for(size_t i = 0; i < frameTranslationSize; ++i) + m[frameTranslation[i][0]] = frameTranslation[i][1]; + return m; +} + +// list of deprecated frames and their successors +static const uint deprecatedFramesSize = 4; +static const char *deprecatedFrames[][2] = { + {"TRDA", "TDRC"}, // 2.3 -> 2.4 (http://en.wikipedia.org/wiki/ID3) + {"TDAT", "TDRC"}, // 2.3 -> 2.4 + {"TYER", "TDRC"}, // 2.3 -> 2.4 + {"TIME", "TDRC"}, // 2.3 -> 2.4 +}; + +Map &deprecationMap() +{ + static Map depMap; + if(depMap.isEmpty()) + for(uint i = 0; i < deprecatedFramesSize; ++i) + depMap[deprecatedFrames[i][0]] = deprecatedFrames[i][1]; + return depMap; +} + +String Frame::frameIDToKey(const ByteVector &id) +{ + Map &m = idMap(); + if(m.contains(id)) + return m[id]; + if(deprecationMap().contains(id)) + return m[deprecationMap()[id]]; + return String::null; +} + +ByteVector Frame::keyToFrameID(const String &s) +{ + static Map m; + if(m.isEmpty()) + for(size_t i = 0; i < frameTranslationSize; ++i) + m[frameTranslation[i][1]] = frameTranslation[i][0]; + if(m.contains(s.upper())) + return m[s]; + return ByteVector::null; +} + +PropertyMap Frame::asProperties() const +{ + if(dynamic_cast< const UnknownFrame *>(this)) { + PropertyMap m; + m.unsupportedData().append("UNKNOWN/" + frameID()); + return m; + } + const ByteVector &id = frameID(); + // workaround until this function is virtual + if(id == "TXXX") + return dynamic_cast< const UserTextIdentificationFrame* >(this)->asProperties(); + else if(id[0] == 'T') + return dynamic_cast< const TextIdentificationFrame* >(this)->asProperties(); + else if(id == "WXXX") + return dynamic_cast< const UserUrlLinkFrame* >(this)->asProperties(); + else if(id[0] == 'W') + return dynamic_cast< const UrlLinkFrame* >(this)->asProperties(); + else if(id == "COMM") + return dynamic_cast< const CommentsFrame* >(this)->asProperties(); + else if(id == "USLT") + return dynamic_cast< const UnsynchronizedLyricsFrame* >(this)->asProperties(); + PropertyMap m; + m.unsupportedData().append(id); + return m; +} + +void Frame::splitProperties(const PropertyMap &original, PropertyMap &singleFrameProperties, + PropertyMap &tiplProperties, PropertyMap &tmclProperties) +{ + + singleFrameProperties.clear(); + tiplProperties.clear(); + tmclProperties.clear(); + for(PropertyMap::ConstIterator it = original.begin(); it != original.end(); ++it) { + if(TextIdentificationFrame::involvedPeopleMap().contains(it->first)) + tiplProperties.insert(it->first, it->second); + else if(it->first.startsWith(TextIdentificationFrame::instrumentPrefix)) + tmclProperties.insert(it->first, it->second); + else + singleFrameProperties.insert(it->first, it->second); + } +} + //////////////////////////////////////////////////////////////////////////////// // Frame::Header class //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/mpeg/id3v2/id3v2frame.h b/taglib/mpeg/id3v2/id3v2frame.h index 2b6bcd88..95c4070b 100644 --- a/taglib/mpeg/id3v2/id3v2frame.h +++ b/taglib/mpeg/id3v2/id3v2frame.h @@ -33,6 +33,7 @@ namespace TagLib { class StringList; + class PropertyMap; namespace ID3v2 { @@ -56,6 +57,14 @@ namespace TagLib { friend class FrameFactory; public: + + /*! + * Creates a textual frame which corresponds to a single key in the PropertyMap + * interface. These are all (User)TextIdentificationFrames except TIPL and TMCL, + * all (User)URLLinkFrames, CommentsFrames, and UnsynchronizedLyricsFrame. + */ + static Frame *createTextualFrame(const String &key, const StringList &values); + /*! * Destroys this Frame instance. */ @@ -126,6 +135,28 @@ namespace TagLib { */ static ByteVector textDelimiter(String::Type t); + /*! + * The string with which an instrument name is prefixed to build a key in a PropertyMap; + * used to translate PropertyMaps to TMCL frames. In the current implementation, this + * is "PERFORMER:". + */ + static const String instrumentPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a COMM frame instead of a TXXX + * frame for a non-standard key. In the current implementation, this is "COMMENT:". + */ + static const String commentPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a USLT frame instead of a TXXX + * frame for a non-standard key. In the current implementation, this is "LYRICS:". + */ + static const String lyricsPrefix; + /*! + * The PropertyMap key prefix which triggers the use of a WXXX frame instead of a TXX + * frame for a non-standard key. In the current implementation, this is "URL:". + */ + static const String urlPrefix; + protected: class Header; @@ -222,6 +253,44 @@ namespace TagLib { String::Type checkTextEncoding(const StringList &fields, String::Type encoding) const; + + /*! + * Parses the contents of this frame as PropertyMap. If that fails, the returend + * PropertyMap will be empty, and its unsupportedData() will contain this frame's + * ID. + * BIC: Will be a virtual function in future releases. + */ + PropertyMap asProperties() const; + + /*! + * Returns an appropriate ID3 frame ID for the given free-form tag key. This method + * will return ByteVector::null if no specialized translation is found. + */ + static ByteVector keyToFrameID(const String &); + + /*! + * Returns a free-form tag name for the given ID3 frame ID. Note that this does not work + * for general frame IDs such as TXXX or WXXX; in such a case String::null is returned. + */ + static String frameIDToKey(const ByteVector &); + + + /*! + * This helper function splits the PropertyMap \a original into three ProperytMaps + * \a singleFrameProperties, \a tiplProperties, and \a tmclProperties, such that: + * - \a singleFrameProperties contains only of keys which can be represented with + * exactly one ID3 frame per key. In the current implementation + * this is everything except for the fixed "involved people" keys and keys of the + * form "TextIdentificationFrame::instrumentPrefix" + "instrument", which are + * mapped to a TMCL frame. + * - \a tiplProperties will consist of those keys that are present in + * TextIdentificationFrame::involvedPeopleMap() + * - \a tmclProperties contains the "musician credits" keys which should be mapped + * to a TMCL frame + */ + static void splitProperties(const PropertyMap &original, PropertyMap &singleFrameProperties, + PropertyMap &tiplProperties, PropertyMap &tmclProperties); + private: Frame(const Frame &); Frame &operator=(const Frame &); diff --git a/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/mpeg/id3v2/id3v2tag.cpp index 5b4c5c5b..ce228330 100644 --- a/taglib/mpeg/id3v2/id3v2tag.cpp +++ b/taglib/mpeg/id3v2/id3v2tag.cpp @@ -24,18 +24,23 @@ ***************************************************************************/ #include -#include #include "id3v2tag.h" #include "id3v2header.h" #include "id3v2extendedheader.h" #include "id3v2footer.h" #include "id3v2synchdata.h" - +#include "tbytevector.h" #include "id3v1genres.h" +#include "tpropertymap.h" +#include #include "frames/textidentificationframe.h" #include "frames/commentsframe.h" +#include "frames/urllinkframe.h" +#include "frames/uniquefileidentifierframe.h" +#include "frames/unsynchronizedlyricsframe.h" +#include "frames/unknownframe.h" using namespace TagLib; using namespace ID3v2; @@ -324,9 +329,99 @@ void ID3v2::Tag::removeFrame(Frame *frame, bool del) void ID3v2::Tag::removeFrames(const ByteVector &id) { - FrameList l = d->frameListMap[id]; - for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) - removeFrame(*it, true); + FrameList l = d->frameListMap[id]; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + removeFrame(*it, true); +} + +PropertyMap ID3v2::Tag::properties() const +{ + PropertyMap properties; + for(FrameList::ConstIterator it = frameList().begin(); it != frameList().end(); ++it) { + PropertyMap props = (*it)->asProperties(); + properties.merge(props); + } + return properties; +} + +void ID3v2::Tag::removeUnsupportedProperties(const StringList &properties) +{ + for(StringList::ConstIterator it = properties.begin(); it != properties.end(); ++it){ + if(it->startsWith("UNKNOWN/")) { + String frameID = it->substr(String("UNKNOWN/").size()); + if(frameID.size() != 4) + continue; // invalid specification + ByteVector id = frameID.data(String::Latin1); + // delete all unknown frames of given type + FrameList l = frameList(id); + for(FrameList::ConstIterator fit = l.begin(); fit != l.end(); fit++) + if (dynamic_cast(*fit) != NULL) + removeFrame(*fit); + } else if(it->size() == 4){ + ByteVector id = it->data(String::Latin1); + removeFrames(id); + } else { + ByteVector id = it->substr(0,4).data(String::Latin1); + if(it->size() <= 5) + continue; // invalid specification + String description = it->substr(5); + Frame *frame; + if(id == "TXXX") + frame = UserTextIdentificationFrame::find(this, description); + else if(id == "WXXX") + frame = UserUrlLinkFrame::find(this, description); + else if(id == "COMM") + frame = CommentsFrame::findByDescription(this, description); + else if(id == "USLT") + frame = UnsynchronizedLyricsFrame::findByDescription(this, description); + if(frame) + removeFrame(frame); + } + } +} + +PropertyMap ID3v2::Tag::setProperties(const PropertyMap &origProps) +{ + FrameList framesToDelete; + // we split up the PropertyMap into the "normal" keys and the "complicated" ones, + // which are those according to TIPL or TMCL frames. + PropertyMap properties; + PropertyMap tiplProperties; + PropertyMap tmclProperties; + Frame::splitProperties(origProps, properties, tiplProperties, tmclProperties); + for(FrameListMap::ConstIterator it = frameListMap().begin(); it != frameListMap().end(); ++it){ + for(FrameList::ConstIterator lit = it->second.begin(); lit != it->second.end(); ++lit){ + PropertyMap frameProperties = (*lit)->asProperties(); + if(it->first == "TIPL") { + if (tiplProperties != frameProperties) + framesToDelete.append(*lit); + else + tiplProperties.erase(frameProperties); + } else if(it->first == "TMCL") { + if (tmclProperties != frameProperties) + framesToDelete.append(*lit); + else + tmclProperties.erase(frameProperties); + } else if(!properties.contains(frameProperties)) + framesToDelete.append(*lit); + else + properties.erase(frameProperties); + } + } + for(FrameList::ConstIterator it = framesToDelete.begin(); it != framesToDelete.end(); ++it) + removeFrame(*it); + + // now create remaining frames: + // start with the involved people list (TIPL) + if(!tiplProperties.isEmpty()) + addFrame(TextIdentificationFrame::createTIPLFrame(tiplProperties)); + // proceed with the musician credit list (TMCL) + if(!tmclProperties.isEmpty()) + addFrame(TextIdentificationFrame::createTMCLFrame(tmclProperties)); + // now create the "one key per frame" frames + for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it) + addFrame(Frame::createTextualFrame(it->first, it->second)); + return PropertyMap(); // ID3 implements the complete PropertyMap interface, so an empty map is returned } ByteVector ID3v2::Tag::render() const diff --git a/taglib/mpeg/id3v2/id3v2tag.h b/taglib/mpeg/id3v2/id3v2tag.h index 4a52854a..94784e76 100644 --- a/taglib/mpeg/id3v2/id3v2tag.h +++ b/taglib/mpeg/id3v2/id3v2tag.h @@ -260,6 +260,56 @@ namespace TagLib { */ void removeFrames(const ByteVector &id); + /*! + * Implements the unified property interface -- export function. + * This function does some work to translate the hard-specified ID3v2 + * frame types into a free-form string-to-stringlist PropertyMap: + * - if ID3v2 frame ID is known by Frame::frameIDToKey(), the returned + * key is used + * - if the frame ID is "TXXX" (user text frame), the description() is + * used as key + * - if the frame ID is "WXXX" (user url frame), + * - if the description is empty or "URL", the key "URL" is used + * - otherwise, the key "URL:" is used; + * - if the frame ID is "COMM" (comments frame), + * - if the description is empty or "COMMENT", the key "COMMENT" + * is used + * - otherwise, the key "COMMENT:" is used; + * - if the frame ID is "USLT" (unsynchronized lyrics), + * - if the description is empty or "LYRICS", the key "LYRICS" is used + * - otherwise, the key "LYRICS:" is used; + * - if the frame ID is "TIPL" (involved peoples list), and if all the + * roles defined in the frame are known in TextIdentificationFrame::involvedPeopleMap(), + * then "=" will be contained in the returned obejct for each + * - if the frame ID is "TMCL" (musician credit list), then + * "PERFORMER:=" will be contained in the returned + * PropertyMap for each defined musician + * In any other case, the unsupportedData() of the returned object will contain + * the frame's ID and, in case of a frame ID which is allowed to appear more than + * once, the description, separated by a "/". + * + */ + PropertyMap properties() const; + + /*! + * Removes unsupported frames given by \a properties. The elements of + * \a properties must be taken from properties().unsupportedData(); they + * are of one of the following forms: + * - a four-character frame ID, if the ID3 specification allows only one + * frame with that ID (thus, the frame is uniquely determined) + * - frameID + "/" + description(), when the ID is one of "TXXX", "WXXX", + * "COMM", or "USLT", + * - "UNKNOWN/" + frameID, for frames that could not be parsed by TagLib. + * In that case, *all* unknown frames with the given ID will be removed. + */ + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified property interface -- import function. + * See the comments in properties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Render the tag back to binary data, suitable to be written to disk. */ diff --git a/taglib/mpeg/mpegfile.cpp b/taglib/mpeg/mpegfile.cpp index a3bad823..28b8fca7 100644 --- a/taglib/mpeg/mpegfile.cpp +++ b/taglib/mpeg/mpegfile.cpp @@ -35,6 +35,7 @@ #include "mpegfile.h" #include "mpegheader.h" +#include "tpropertymap.h" using namespace TagLib; @@ -133,6 +134,40 @@ TagLib::Tag *MPEG::File::tag() const return &d->tag; } +PropertyMap MPEG::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->properties(); + if(d->hasAPE) + return d->tag.access(APEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +void MPEG::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->hasID3v2) + d->tag.access(ID3v2Index, false)->removeUnsupportedProperties(properties); + else if(d->hasAPE) + d->tag.access(APEIndex, false)->removeUnsupportedProperties(properties); + else if(d->hasID3v1) + d->tag.access(ID3v1Index, false)->removeUnsupportedProperties(properties); +} +PropertyMap MPEG::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->setProperties(properties); + else if(d->hasAPE) + return d->tag.access(APEIndex, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(ID3v2Index, true)->setProperties(properties); +} + MPEG::Properties *MPEG::File::audioProperties() const { return d->properties; diff --git a/taglib/mpeg/mpegfile.h b/taglib/mpeg/mpegfile.h index cff5469d..dbd0e017 100644 --- a/taglib/mpeg/mpegfile.h +++ b/taglib/mpeg/mpegfile.h @@ -28,6 +28,7 @@ #include "taglib_export.h" #include "tfile.h" +#include "tag.h" #include "mpegproperties.h" @@ -128,6 +129,23 @@ namespace TagLib { */ virtual Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains more than one tag, only the + * first one (in the order ID3v2, APE, ID3v1) will be converted to the + * PropertyMap. + */ + PropertyMap properties() const; + + void removeUnsupportedProperties(const StringList &properties); + + /*! + * Implements the unified tag dictionary interface -- import function. + * As with the export, only one tag is taken into account. If the file + * has no tag at all, ID3v2 will be created. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the MPEG::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/ogg/vorbis/vorbisfile.cpp b/taglib/ogg/vorbis/vorbisfile.cpp index 60056f83..e2eed9e2 100644 --- a/taglib/ogg/vorbis/vorbisfile.cpp +++ b/taglib/ogg/vorbis/vorbisfile.cpp @@ -27,9 +27,11 @@ #include #include +#include #include "vorbisfile.h" + using namespace TagLib; class Vorbis::File::FilePrivate @@ -85,6 +87,16 @@ Ogg::XiphComment *Vorbis::File::tag() const return d->comment; } +PropertyMap Vorbis::File::properties() const +{ + return d->comment->properties(); +} + +PropertyMap Vorbis::File::setProperties(const PropertyMap &properties) +{ + return d->comment->setProperties(properties); +} + Vorbis::Properties *Vorbis::File::audioProperties() const { return d->properties; diff --git a/taglib/ogg/vorbis/vorbisfile.h b/taglib/ogg/vorbis/vorbisfile.h index 299d9c2d..15c29d99 100644 --- a/taglib/ogg/vorbis/vorbisfile.h +++ b/taglib/ogg/vorbis/vorbisfile.h @@ -90,6 +90,19 @@ namespace TagLib { */ virtual Ogg::XiphComment *tag() const; + + /*! + * Implements the unified property interface -- export function. + * This forwards directly to XiphComment::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + * Like properties(), this is a forwarder to the file's XiphComment. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the Vorbis::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/ogg/xiphcomment.cpp b/taglib/ogg/xiphcomment.cpp index c02ef1a1..5a6feef3 100644 --- a/taglib/ogg/xiphcomment.cpp +++ b/taglib/ogg/xiphcomment.cpp @@ -27,6 +27,7 @@ #include #include +#include using namespace TagLib; @@ -188,6 +189,44 @@ const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const return d->fieldListMap; } +PropertyMap Ogg::XiphComment::properties() const +{ + return d->fieldListMap; +} + +PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties) +{ + // check which keys are to be deleted + StringList toRemove; + for(FieldListMap::ConstIterator it = d->fieldListMap.begin(); it != d->fieldListMap.end(); ++it) + if (!properties.contains(it->first)) + toRemove.append(it->first); + + for(StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it) + removeField(*it); + + // now go through keys in \a properties and check that the values match those in the xiph comment */ + PropertyMap::ConstIterator it = properties.begin(); + for(; it != properties.end(); ++it) + { + if(!d->fieldListMap.contains(it->first) || !(it->second == d->fieldListMap[it->first])) { + const StringList &sl = it->second; + if(sl.size() == 0) + // zero size string list -> remove the tag with all values + removeField(it->first); + else { + // replace all strings in the list for the tag + StringList::ConstIterator valueIterator = sl.begin(); + addField(it->first, *valueIterator, true); + ++valueIterator; + for(; valueIterator != sl.end(); ++valueIterator) + addField(it->first, *valueIterator, false); + } + } + } + return PropertyMap(); +} + String Ogg::XiphComment::vendorID() const { return d->vendorID; diff --git a/taglib/ogg/xiphcomment.h b/taglib/ogg/xiphcomment.h index b105dd6a..3b44086e 100644 --- a/taglib/ogg/xiphcomment.h +++ b/taglib/ogg/xiphcomment.h @@ -140,6 +140,21 @@ namespace TagLib { */ const FieldListMap &fieldListMap() const; + /*! + * Implements the unified property interface -- export function. + * The result is a one-to-one match of the Xiph comment, since it is + * completely compatible with the property interface (in fact, a Xiph + * comment is nothing more than a map from tag names to list of values, + * as is the dict interface). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * The tags from the given map will be stored one-to-one in the file. + */ + PropertyMap setProperties(const PropertyMap&); + /*! * Returns the vendor ID of the Ogg Vorbis encoder. libvorbis 1.0 as the * most common case always returns "Xiph.Org libVorbis I 20020717". diff --git a/taglib/riff/aiff/aifffile.cpp b/taglib/riff/aiff/aifffile.cpp index b868ada4..ece84f05 100644 --- a/taglib/riff/aiff/aifffile.cpp +++ b/taglib/riff/aiff/aifffile.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include "aifffile.h" @@ -83,6 +85,17 @@ ID3v2::Tag *RIFF::AIFF::File::tag() const return d->tag; } +PropertyMap RIFF::AIFF::File::properties() const +{ + return d->tag->properties(); +} + +PropertyMap RIFF::AIFF::File::setProperties(const PropertyMap &properties) +{ + return d->tag->setProperties(properties); +} + + RIFF::AIFF::Properties *RIFF::AIFF::File::audioProperties() const { return d->properties; diff --git a/taglib/riff/aiff/aifffile.h b/taglib/riff/aiff/aifffile.h index cac42934..a50c8ecb 100644 --- a/taglib/riff/aiff/aifffile.h +++ b/taglib/riff/aiff/aifffile.h @@ -83,6 +83,18 @@ namespace TagLib { */ virtual ID3v2::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * This method forwards to ID3v2::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * This method forwards to ID3v2::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the AIFF::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/riff/wav/wavfile.cpp b/taglib/riff/wav/wavfile.cpp index 107d2b45..613db4ef 100644 --- a/taglib/riff/wav/wavfile.cpp +++ b/taglib/riff/wav/wavfile.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include "wavfile.h" @@ -83,6 +85,17 @@ ID3v2::Tag *RIFF::WAV::File::tag() const return d->tag; } +PropertyMap RIFF::WAV::File::properties() const +{ + return d->tag->properties(); +} + +PropertyMap RIFF::WAV::File::setProperties(const PropertyMap &properties) +{ + return d->tag->setProperties(properties); +} + + RIFF::WAV::Properties *RIFF::WAV::File::audioProperties() const { return d->properties; diff --git a/taglib/riff/wav/wavfile.h b/taglib/riff/wav/wavfile.h index 341932b9..861f3f77 100644 --- a/taglib/riff/wav/wavfile.h +++ b/taglib/riff/wav/wavfile.h @@ -83,6 +83,18 @@ namespace TagLib { */ virtual ID3v2::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * This method forwards to ID3v2::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * This method forwards to ID3v2::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the WAV::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/s3m/s3mfile.cpp b/taglib/s3m/s3mfile.cpp index 0c8a712d..98bc6a56 100644 --- a/taglib/s3m/s3mfile.cpp +++ b/taglib/s3m/s3mfile.cpp @@ -23,6 +23,7 @@ #include "tstringlist.h" #include "tdebug.h" #include "modfileprivate.h" +#include "tpropertymap.h" #include @@ -67,6 +68,16 @@ Mod::Tag *S3M::File::tag() const return &d->tag; } +PropertyMap S3M::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap S3M::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + S3M::Properties *S3M::File::audioProperties() const { return &d->properties; diff --git a/taglib/s3m/s3mfile.h b/taglib/s3m/s3mfile.h index 6eb938a3..0605b2bf 100644 --- a/taglib/s3m/s3mfile.h +++ b/taglib/s3m/s3mfile.h @@ -60,6 +60,18 @@ namespace TagLib { Mod::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the S3M::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/tag.cpp b/taglib/tag.cpp index 8be33c80..67634081 100644 --- a/taglib/tag.cpp +++ b/taglib/tag.cpp @@ -24,6 +24,8 @@ ***************************************************************************/ #include "tag.h" +#include "tstringlist.h" +#include "tpropertymap.h" using namespace TagLib; @@ -53,6 +55,101 @@ bool Tag::isEmpty() const track() == 0); } +PropertyMap Tag::properties() const +{ + PropertyMap map; + if(!(title().isNull())) + map["TITLE"].append(title()); + if(!(artist().isNull())) + map["ARTIST"].append(artist()); + if(!(album().isNull())) + map["ALBUM"].append(album()); + if(!(comment().isNull())) + map["COMMENT"].append(comment()); + if(!(genre().isNull())) + map["GENRE"].append(genre()); + if(!(year() == 0)) + map["DATE"].append(String::number(year())); + if(!(track() == 0)) + map["TRACKNUMBER"].append(String::number(track())); + return map; +} + +void Tag::removeUnsupportedProperties(const StringList&) +{ +} + +PropertyMap Tag::setProperties(const PropertyMap &origProps) +{ + PropertyMap properties(origProps); + properties.removeEmpty(); + StringList oneValueSet; + // can this be simplified by using some preprocessor defines / function pointers? + if(properties.contains("TITLE")) { + setTitle(properties["TITLE"].front()); + oneValueSet.append("TITLE"); + } else + setTitle(String::null); + + if(properties.contains("ARTIST")) { + setArtist(properties["ARTIST"].front()); + oneValueSet.append("ARTIST"); + } else + setArtist(String::null); + + if(properties.contains("ALBUM")) { + setAlbum(properties["ALBUM"].front()); + oneValueSet.append("ALBUM"); + } else + setAlbum(String::null); + + if(properties.contains("COMMENT")) { + setComment(properties["COMMENT"].front()); + oneValueSet.append("COMMENT"); + } else + setComment(String::null); + + if(properties.contains("GENRE")) { + setGenre(properties["GENRE"].front()); + oneValueSet.append("GENRE"); + } else + setGenre(String::null); + + if(properties.contains("DATE")) { + bool ok; + int date = properties["DATE"].front().toInt(&ok); + if(ok) { + setYear(date); + oneValueSet.append("DATE"); + } else + setYear(0); + } + else + setYear(0); + + if(properties.contains("TRACKNUMBER")) { + bool ok; + int track = properties["TRACKNUMBER"].front().toInt(&ok); + if(ok) { + setTrack(track); + oneValueSet.append("TRACKNUMBER"); + } else + setTrack(0); + } + else + setYear(0); + + // for each tag that has been set above, remove the first entry in the corresponding + // value list. The others will be returned as unsupported by this format. + for(StringList::Iterator it = oneValueSet.begin(); it != oneValueSet.end(); ++it) { + if(properties[*it].size() == 1) + properties.erase(*it); + else + properties[*it].erase( properties[*it].begin() ); + } + return properties; +} + void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static { if(overwrite) { diff --git a/taglib/tag.h b/taglib/tag.h index c8f12a85..76c9a82a 100644 --- a/taglib/tag.h +++ b/taglib/tag.h @@ -41,6 +41,8 @@ namespace TagLib { * in TagLib::AudioProperties, TagLib::File and TagLib::FileRef. */ + class PropertyMap; + class TAGLIB_EXPORT Tag { public: @@ -50,6 +52,32 @@ namespace TagLib { */ virtual ~Tag(); + /*! + * Exports the tags of the file as dictionary mapping (human readable) tag + * names (Strings) to StringLists of tag values. + * The default implementation in this class considers only the usual built-in + * tags (artist, album, ...) and only one value per key. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties, or a subset of them, from the tag. + * The parameter \a properties must contain only entries from + * properties().unsupportedData(). + * BIC: Will become virtual in future releases. Currently the non-virtual + * standard implementation of TagLib::Tag does nothing, since there are + * no unsupported elements. + */ + void removeUnsupportedProperties(const StringList& properties); + + /*! + * Sets the tags of this File to those specified in \a properties. This default + * implementation sets only the tags for which setter methods exist in this class + * (artist, album, ...), and only one value per key; the rest will be contained + * in the returned PropertyMap. + */ + PropertyMap setProperties(const PropertyMap &properties); + /*! * Returns the track name; if no track name is present in the tag * String::null will be returned. diff --git a/taglib/tagunion.cpp b/taglib/tagunion.cpp index 4a9978d0..52d7136b 100644 --- a/taglib/tagunion.cpp +++ b/taglib/tagunion.cpp @@ -24,6 +24,7 @@ ***************************************************************************/ #include "tagunion.h" +#include "tstringlist.h" using namespace TagLib; diff --git a/taglib/toolkit/tfile.cpp b/taglib/toolkit/tfile.cpp index ae3eec1d..09b8fdbc 100644 --- a/taglib/toolkit/tfile.cpp +++ b/taglib/toolkit/tfile.cpp @@ -27,6 +27,7 @@ #include "tfilestream.h" #include "tstring.h" #include "tdebug.h" +#include "tpropertymap.h" #include #include @@ -50,6 +51,24 @@ # define W_OK 2 #endif +#include "asffile.h" +#include "mpegfile.h" +#include "vorbisfile.h" +#include "flacfile.h" +#include "oggflacfile.h" +#include "mpcfile.h" +#include "mp4file.h" +#include "wavpackfile.h" +#include "speexfile.h" +#include "trueaudiofile.h" +#include "aifffile.h" +#include "wavfile.h" +#include "apefile.h" +#include "modfile.h" +#include "s3mfile.h" +#include "itfile.h" +#include "xmfile.h" + using namespace TagLib; class File::FilePrivate @@ -95,6 +114,118 @@ FileName File::name() const return d->stream->name(); } +PropertyMap File::properties() const +{ + // ugly workaround until this method is virtual + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + if(dynamic_cast(this)) + return dynamic_cast(this)->properties(); + // no specialized implementation available -> use generic one + // - ASF: ugly format, largely undocumented, not worth implementing + // dict interface ... + // - MP4: taglib's MP4::Tag does not really support anything beyond + // the basic implementation, therefor we use just the default Tag + // interface + return tag()->properties(); +} + +void File::removeUnsupportedProperties(const StringList &properties) +{ + // here we only consider those formats that could possibly contain + // unsupported properties + if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else if(dynamic_cast(this)) + dynamic_cast(this)->removeUnsupportedProperties(properties); + else + tag()->removeUnsupportedProperties(properties); +} + +PropertyMap File::setProperties(const PropertyMap &properties) +{ + if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else if(dynamic_cast(this)) + return dynamic_cast(this)->setProperties(properties); + else + return tag()->setProperties(properties); +} + ByteVector File::readBlock(ulong length) { return d->stream->readBlock(length); diff --git a/taglib/toolkit/tfile.h b/taglib/toolkit/tfile.h index ee6f0488..7df774a0 100644 --- a/taglib/toolkit/tfile.h +++ b/taglib/toolkit/tfile.h @@ -28,6 +28,7 @@ #include "taglib_export.h" #include "taglib.h" +#include "tag.h" #include "tbytevector.h" #include "tiostream.h" @@ -36,6 +37,7 @@ namespace TagLib { class String; class Tag; class AudioProperties; + class PropertyMap; //! A file class with some useful methods for tag manipulation @@ -76,6 +78,36 @@ namespace TagLib { */ virtual Tag *tag() const = 0; + /*! + * Exports the tags of the file as dictionary mapping (human readable) tag + * names (Strings) to StringLists of tag values. Calls the according specialization + * in the File subclasses. + * For each metadata object of the file that could not be parsed into the PropertyMap + * format, the returend map's unsupportedData() list will contain one entry identifying + * that object (e.g. the frame type for ID3v2 tags). Use removeUnsupportedProperties() + * to remove (a subset of) them. + * BIC: Will be made virtual in future releases. + */ + PropertyMap properties() const; + + /*! + * Removes unsupported properties, or a subset of them, from the file's metadata. + * The parameter \a properties must contain only entries from + * properties().unsupportedData(). + * BIC: Will be mad virtual in future releases. + */ + void removeUnsupportedProperties(const StringList& properties); + + /*! + * Sets the tags of this File to those specified in \a properties. Calls the + * according specialization method in the subclasses of File to do the translation + * into the format-specific details. + * If some value(s) could not be written imported to the specific metadata format, + * the returned PropertyMap will contain those value(s). Otherwise it will be empty, + * indicating that no problems occured. + * BIC: will become pure virtual in the future + */ + PropertyMap setProperties(const PropertyMap &properties); /*! * Returns a pointer to this file's audio properties. This should be * reimplemented in the concrete subclasses. If no audio properties were diff --git a/taglib/toolkit/tlist.h b/taglib/toolkit/tlist.h index dce0e1c6..0099dad5 100644 --- a/taglib/toolkit/tlist.h +++ b/taglib/toolkit/tlist.h @@ -227,6 +227,11 @@ namespace TagLib { */ bool operator==(const List &l) const; + /*! + * Compares this list with \a l and returns true if the lists differ. + */ + bool operator!=(const List &l) const; + protected: /* * If this List is being shared via implicit sharing, do a deep copy of the diff --git a/taglib/toolkit/tlist.tcc b/taglib/toolkit/tlist.tcc index a11887d8..37817f05 100644 --- a/taglib/toolkit/tlist.tcc +++ b/taglib/toolkit/tlist.tcc @@ -300,6 +300,12 @@ bool List::operator==(const List &l) const return d->list == l.d->list; } +template +bool List::operator!=(const List &l) const +{ + return d->list != l.d->list; +} + //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/toolkit/tpropertymap.cpp b/taglib/toolkit/tpropertymap.cpp new file mode 100644 index 00000000..1743a0b2 --- /dev/null +++ b/taglib/toolkit/tpropertymap.cpp @@ -0,0 +1,202 @@ +/*************************************************************************** + copyright : (C) 2012 by Michael Helmling + email : helmling@mathematik.uni-kl.de + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#include "tpropertymap.h" +using namespace TagLib; + + +PropertyMap::PropertyMap() : SimplePropertyMap() +{ +} + +PropertyMap::PropertyMap(const PropertyMap &m) : SimplePropertyMap(m), unsupported(m.unsupported) +{ +} + +PropertyMap::PropertyMap(const SimplePropertyMap &m) +{ + for(SimplePropertyMap::ConstIterator it = m.begin(); it != m.end(); ++it){ + String key = prepareKey(it->first); + if(!key.isNull()) + insert(it->first, it->second); + else + unsupported.append(it->first); + } +} + +PropertyMap::~PropertyMap() +{ +} + +bool PropertyMap::insert(const String &key, const StringList &values) +{ + String realKey = prepareKey(key); + if(realKey.isNull()) + return false; + + Iterator result = SimplePropertyMap::find(realKey); + if(result == end()) + SimplePropertyMap::insert(realKey, values); + else + SimplePropertyMap::operator[](realKey).append(values); + return true; +} + +bool PropertyMap::replace(const String &key, const StringList &values) +{ + String realKey = prepareKey(key); + if(realKey.isNull()) + return false; + SimplePropertyMap::erase(realKey); + SimplePropertyMap::insert(realKey, values); + return true; +} + +PropertyMap::Iterator PropertyMap::find(const String &key) +{ + String realKey = prepareKey(key); + if(realKey.isNull()) + return end(); + return SimplePropertyMap::find(realKey); +} + +PropertyMap::ConstIterator PropertyMap::find(const String &key) const +{ + String realKey = prepareKey(key); + if(realKey.isNull()) + return end(); + return SimplePropertyMap::find(realKey); +} + +bool PropertyMap::contains(const String &key) const +{ + String realKey = prepareKey(key); + if(realKey.isNull()) + return false; + return SimplePropertyMap::contains(realKey); +} + +bool PropertyMap::contains(const PropertyMap &other) const +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) { + if(!SimplePropertyMap::contains(it->first)) + return false; + if ((*this)[it->first] != it->second) + return false; + } + return true; +} + +PropertyMap &PropertyMap::erase(const String &key) +{ + String realKey = prepareKey(key); + if(!realKey.isNull()) + SimplePropertyMap::erase(realKey); + return *this; +} + +PropertyMap &PropertyMap::erase(const PropertyMap &other) +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) + erase(it->first); + return *this; +} + +PropertyMap &PropertyMap::merge(const PropertyMap &other) +{ + for(PropertyMap::ConstIterator it = other.begin(); it != other.end(); ++it) { + insert(it->first, it->second); + } + unsupported.append(other.unsupported); + return *this; +} + +const StringList &PropertyMap::operator[](const String &key) const +{ + String realKey = prepareKey(key); + return SimplePropertyMap::operator[](realKey); +} + +StringList &PropertyMap::operator[](const String &key) +{ + String realKey = prepareKey(key); + return SimplePropertyMap::operator[](realKey); +} + +bool PropertyMap::operator==(const PropertyMap &other) const +{ + for(ConstIterator it = other.begin(); it != other.end(); ++it) { + ConstIterator thisFind = find(it->first); + if( thisFind == end() || (thisFind->second != it->second) ) + return false; + } + for(ConstIterator it = begin(); it != end(); ++it) { + ConstIterator otherFind = other.find(it->first); + if( otherFind == other.end() || (otherFind->second != it->second) ) + return false; + } + return unsupported == other.unsupported; +} + +bool PropertyMap::operator!=(const PropertyMap &other) const +{ + return !(*this == other); +} + +String PropertyMap::toString() const +{ + String ret = ""; + for(ConstIterator it = begin(); it != end(); ++it) + ret += it->first+"="+it->second.toString(", ") + "\n"; + if(!unsupported.isEmpty()) + ret += "Unsupported Data: " + unsupported.toString(", ") + "\n"; + return ret; +} + +void PropertyMap::removeEmpty() +{ + StringList emptyKeys; + for(Iterator it = begin(); it != end(); ++it) + if(it->second.isEmpty()) + emptyKeys.append(it->first); + for(StringList::Iterator emptyIt = emptyKeys.begin(); emptyIt != emptyKeys.end(); emptyIt++ ) + erase(*emptyIt); +} + +StringList &PropertyMap::unsupportedData() +{ + return unsupported; +} + +const StringList &PropertyMap::unsupportedData() const +{ + return unsupported; +} + +String PropertyMap::prepareKey(const String &proposed) { + if(proposed.isEmpty()) + return String::null; + for (String::ConstIterator it = proposed.begin(); it != proposed.end(); it++) + // forbid non-printable, non-ascii, '=' (#61) and '~' (#126) + if (*it < 32 || *it >= 128 || *it == 61 || *it == 126) + return String::null; + return proposed.upper(); +} diff --git a/taglib/toolkit/tpropertymap.h b/taglib/toolkit/tpropertymap.h new file mode 100644 index 00000000..b955739b --- /dev/null +++ b/taglib/toolkit/tpropertymap.h @@ -0,0 +1,184 @@ +/*************************************************************************** + copyright : (C) 2012 by Michael Helmling + email : helmling@mathematik.uni-kl.de + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301 USA * + ***************************************************************************/ + +#ifndef PROPERTYMAP_H_ +#define PROPERTYMAP_H_ + +#include "tmap.h" +#include "tstringlist.h" + +namespace TagLib { + + typedef Map SimplePropertyMap; + + //! A map for format-independent tag representations. + + /*! + * This map implements a generic representation of textual audio metadata + * ("tags") realized as pairs of a case-insensitive key + * and a nonempty list of corresponding values, each value being an an arbitrary + * unicode String. + * The key has the same restrictions as in the vorbis comment specification, + * i.e. it must contain at least one character; all printable ASCII characters + * except '=' and '~' are allowed. + * + * In order to be safe with other formats, keep these additional restrictions in mind: + * + * - APE only allows keys from 2 to 16 printable ASCII characters (including space), + * with the exception of these strings: ID3, TAG, OggS, MP+ + * + */ + + class TAGLIB_EXPORT PropertyMap: public SimplePropertyMap + { + public: + + typedef SimplePropertyMap::Iterator Iterator; + typedef SimplePropertyMap::ConstIterator ConstIterator; + + PropertyMap(); + + PropertyMap(const PropertyMap &m); + + /*! + * Creates a PropertyMap initialized from a SimplePropertyMap. Copies all + * entries from \a m that have valid keys. + * Invalid keys will be appended to the unsupportedData() list. + */ + PropertyMap(const SimplePropertyMap &m); + + virtual ~PropertyMap(); + + /*! + * Inserts \a values under \a key in the map. If \a key already exists, + * then \values will be appended to the existing StringList. + * The returned value indicates success, i.e. whether \a key is a + * valid key. + */ + bool insert(const String &key, const StringList &values); + + /*! + * Replaces any existing values for \a key with the given \a values, + * and simply insert them if \a key did not exist before. + * The returned value indicates success, i.e. whether \a key is a + * valid key. + */ + bool replace(const String &key, const StringList &values); + + /*! + * Find the first occurrence of \a key. + */ + Iterator find(const String &key); + + /*! + * Find the first occurrence of \a key. + */ + ConstIterator find(const String &key) const; + + /*! + * Returns true if the map contains values for \a key. + */ + bool contains(const String &key) const; + + /*! + * Returns true if this map contains all keys of \a other + * and the values coincide for that keys. Does not take + * the unsupportedData list into account. + */ + bool contains(const PropertyMap &other) const; + + /*! + * Erase the \a key and its values from the map. + */ + PropertyMap &erase(const String &key); + + /*! + * Erases from this map all keys that appear in \a other. + */ + PropertyMap &erase(const PropertyMap &other); + + /*! + * Merge the contents of \a other into this PropertyMap. + * If a key is contained in both maps, the values of the second + * are appended to that of the first. + * The unsupportedData() lists are concatenated as well. + */ + PropertyMap &merge(const PropertyMap &other); + + /*! + * Returns a reference to the value associated with \a key. + * + * \note: This has undefined behavior if the key is not valid or not + * present in the map. + */ + const StringList &operator[](const String &key) const; + + /*! + * Returns a reference to the value associated with \a key. + * + * \note: This has undefined behavior if the key is not valid or not + * present in the map. + */ + StringList &operator[](const String &key); + + /*! + * Returns true if and only if \other has the same contents as this map. + */ + bool operator==(const PropertyMap &other) const; + + /*! + * Returns false if and only \other has the same contents as this map. + */ + bool operator!=(const PropertyMap &other) const; + + /*! + * If a PropertyMap is read from a File object using File::properties(), + * the StringList returned from this function will represent metadata + * that could not be parsed into the PropertyMap representation. This could + * be e.g. binary data, unknown ID3 frames, etc. + * You can remove items from the returned list, which tells TagLib to remove + * those unsupported elements if you call File::setProperties() with the + * same PropertyMap as argument. + */ + StringList &unsupportedData(); + const StringList &unsupportedData() const; + + /*! + * Removes all entries which have an empty value list. + */ + void removeEmpty(); + + String toString() const; + + /*! + * Converts \a proposed into another String suitable to be used as + * a key, or returns String::null if this is not possible. + */ + static String prepareKey(const String &proposed); + + private: + + + StringList unsupported; + }; + +} +#endif /* PROPERTYMAP_H_ */ diff --git a/taglib/trueaudio/trueaudiofile.cpp b/taglib/trueaudio/trueaudiofile.cpp index b584b7fd..e10f6fa5 100644 --- a/taglib/trueaudio/trueaudiofile.cpp +++ b/taglib/trueaudio/trueaudiofile.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include "trueaudiofile.h" #include "id3v1tag.h" @@ -126,6 +128,27 @@ TagLib::Tag *TrueAudio::File::tag() const return &d->tag; } +PropertyMap TrueAudio::File::properties() const +{ + // once Tag::properties() is virtual, this case distinction could actually be done + // within TagUnion. + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +PropertyMap TrueAudio::File::setProperties(const PropertyMap &properties) +{ + if(d->hasID3v2) + return d->tag.access(ID3v2Index, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(ID3v2Index, true)->setProperties(properties); +} + TrueAudio::Properties *TrueAudio::File::audioProperties() const { return d->properties; diff --git a/taglib/trueaudio/trueaudiofile.h b/taglib/trueaudio/trueaudiofile.h index 9c866233..9b0378f7 100644 --- a/taglib/trueaudio/trueaudiofile.h +++ b/taglib/trueaudio/trueaudiofile.h @@ -124,6 +124,20 @@ namespace TagLib { */ virtual TagLib::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains both ID3v1 and v2 tags, only ID3v2 will be + * converted to the PropertyMap. + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * As with the export, only one tag is taken into account. If the file + * has no tag at all, ID3v2 will be created. + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the TrueAudio::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/wavpack/wavpackfile.cpp b/taglib/wavpack/wavpackfile.cpp index 19e4c77d..2addabae 100644 --- a/taglib/wavpack/wavpackfile.cpp +++ b/taglib/wavpack/wavpackfile.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "wavpackfile.h" #include "id3v1tag.h" @@ -105,6 +106,25 @@ TagLib::Tag *WavPack::File::tag() const return &d->tag; } +PropertyMap WavPack::File::properties() const +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->properties(); + if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->properties(); + return PropertyMap(); +} + +PropertyMap WavPack::File::setProperties(const PropertyMap &properties) +{ + if(d->hasAPE) + return d->tag.access(APEIndex, false)->setProperties(properties); + else if(d->hasID3v1) + return d->tag.access(ID3v1Index, false)->setProperties(properties); + else + return d->tag.access(APE, true)->setProperties(properties); +} + WavPack::Properties *WavPack::File::audioProperties() const { return d->properties; diff --git a/taglib/wavpack/wavpackfile.h b/taglib/wavpack/wavpackfile.h index 5173c136..02bac023 100644 --- a/taglib/wavpack/wavpackfile.h +++ b/taglib/wavpack/wavpackfile.h @@ -106,6 +106,20 @@ namespace TagLib { */ virtual TagLib::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * If the file contains both an APE and an ID3v1 tag, only APE + * will be converted to the PropertyMap. + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * As for the export, only one tag is taken into account. If the file + * has no tag at all, APE will be created. + */ + PropertyMap setProperties(const PropertyMap&); + /*! * Returns the MPC::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/xm/xmfile.cpp b/taglib/xm/xmfile.cpp index 17aeab0a..272e5fe0 100644 --- a/taglib/xm/xmfile.cpp +++ b/taglib/xm/xmfile.cpp @@ -23,6 +23,7 @@ #include "tdebug.h" #include "xmfile.h" #include "modfileprivate.h" +#include "tpropertymap.h" #include #include @@ -379,6 +380,16 @@ Mod::Tag *XM::File::tag() const return &d->tag; } +PropertyMap XM::File::properties() const +{ + return d->tag.properties(); +} + +PropertyMap XM::File::setProperties(const PropertyMap &properties) +{ + return d->tag.setProperties(properties); +} + XM::Properties *XM::File::audioProperties() const { return &d->properties; diff --git a/taglib/xm/xmfile.h b/taglib/xm/xmfile.h index a4ae7244..1b07010b 100644 --- a/taglib/xm/xmfile.h +++ b/taglib/xm/xmfile.h @@ -60,6 +60,18 @@ namespace TagLib { Mod::Tag *tag() const; + /*! + * Implements the unified property interface -- export function. + * Forwards to Mod::Tag::properties(). + */ + PropertyMap properties() const; + + /*! + * Implements the unified property interface -- import function. + * Forwards to Mod::Tag::setProperties(). + */ + PropertyMap setProperties(const PropertyMap &); + /*! * Returns the XM::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/tests/data/rare_frames.mp3 b/tests/data/rare_frames.mp3 new file mode 100644 index 00000000..e485337f Binary files /dev/null and b/tests/data/rare_frames.mp3 differ diff --git a/tests/data/test.ogg b/tests/data/test.ogg new file mode 100644 index 00000000..220f76f0 Binary files /dev/null and b/tests/data/test.ogg differ diff --git a/tests/test_apetag.cpp b/tests/test_apetag.cpp index 901a2aaf..422725df 100644 --- a/tests/test_apetag.cpp +++ b/tests/test_apetag.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include "utils.h" using namespace std; @@ -15,6 +17,8 @@ class TestAPETag : public CppUnit::TestFixture CPPUNIT_TEST_SUITE(TestAPETag); CPPUNIT_TEST(testIsEmpty); CPPUNIT_TEST(testIsEmpty2); + CPPUNIT_TEST(testPropertyInterface1); + CPPUNIT_TEST(testPropertyInterface2); CPPUNIT_TEST_SUITE_END(); public: @@ -35,6 +39,50 @@ public: CPPUNIT_ASSERT(!tag.isEmpty()); } + void testPropertyInterface1() + { + APE::Tag tag; + PropertyMap dict = tag.properties(); + CPPUNIT_ASSERT(dict.isEmpty()); + dict["ARTIST"] = String("artist 1"); + dict["ARTIST"].append("artist 2"); + dict["TRACKNUMBER"].append("17"); + tag.setProperties(dict); + CPPUNIT_ASSERT_EQUAL(String("17"), tag.itemListMap()["TRACK"].values()[0]); + CPPUNIT_ASSERT_EQUAL(2u, tag.itemListMap()["ARTIST"].values().size()); + CPPUNIT_ASSERT_EQUAL(String("artist 1"), tag.artist()); + CPPUNIT_ASSERT_EQUAL(17u, tag.track()); + } + + void testPropertyInterface2() + { + APE::Tag tag; + APE::Item item1 = APE::Item("TRACK", "17"); + tag.setItem("TRACK", item1); + + APE::Item item2 = APE::Item(); + item2.setType(APE::Item::Binary); + tag.setItem("TESTBINARY", item2); + + PropertyMap properties = tag.properties(); + CPPUNIT_ASSERT_EQUAL(1u, properties.unsupportedData().size()); + CPPUNIT_ASSERT(properties.contains("TRACKNUMBER")); + CPPUNIT_ASSERT(!properties.contains("TRACK")); + CPPUNIT_ASSERT(tag.itemListMap().contains("TESTBINARY")); + + tag.removeUnsupportedProperties(properties.unsupportedData()); + CPPUNIT_ASSERT(!tag.itemListMap().contains("TESTBINARY")); + + APE::Item item3 = APE::Item("TRACKNUMBER", "29"); + tag.setItem("TRACKNUMBER", item3); + properties = tag.properties(); + CPPUNIT_ASSERT_EQUAL(2u, properties["TRACKNUMBER"].size()); + CPPUNIT_ASSERT_EQUAL(String("17"), properties["TRACKNUMBER"][0]); + CPPUNIT_ASSERT_EQUAL(String("29"), properties["TRACKNUMBER"][1]); + + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestAPETag); + diff --git a/tests/test_flac.cpp b/tests/test_flac.cpp index 90baa844..1bf6015a 100644 --- a/tests/test_flac.cpp +++ b/tests/test_flac.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "utils.h" @@ -22,6 +23,7 @@ class TestFLAC : public CppUnit::TestFixture CPPUNIT_TEST(testRemoveAllPictures); CPPUNIT_TEST(testRepeatedSave); CPPUNIT_TEST(testSaveMultipleValues); + CPPUNIT_TEST(testDict); CPPUNIT_TEST_SUITE_END(); public: @@ -208,6 +210,27 @@ public: CPPUNIT_ASSERT_EQUAL(String("artist 2"), m["ARTIST"][1]); } + void testDict() + { + // test unicode & multiple values with dict interface + ScopedFileCopy copy("silence-44-s", ".flac"); + string newname = copy.fileName(); + + FLAC::File *f = new FLAC::File(newname.c_str()); + PropertyMap dict; + dict["ARTIST"].append("artøst 1"); + dict["ARTIST"].append("artöst 2"); + f->setProperties(dict); + f->save(); + delete f; + + f = new FLAC::File(newname.c_str()); + dict = f->properties(); + CPPUNIT_ASSERT_EQUAL(TagLib::uint(2), dict["ARTIST"].size()); + CPPUNIT_ASSERT_EQUAL(String("artøst 1"), dict["ARTIST"][0]); + CPPUNIT_ASSERT_EQUAL(String("artöst 2"), dict["ARTIST"][1]); + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestFLAC); diff --git a/tests/test_id3v2.cpp b/tests/test_id3v2.cpp index 5b2151ad..49ffef0d 100644 --- a/tests/test_id3v2.cpp +++ b/tests/test_id3v2.cpp @@ -10,11 +10,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include "utils.h" using namespace std; @@ -67,6 +69,8 @@ class TestID3v2 : public CppUnit::TestFixture CPPUNIT_TEST(testDowngradeTo23); // CPPUNIT_TEST(testUpdateFullDate22); TODO TYE+TDA should be upgraded to TDRC together CPPUNIT_TEST(testCompressedFrameWithBrokenLength); + CPPUNIT_TEST(testPropertyInterface); + CPPUNIT_TEST(testPropertyInterface2); CPPUNIT_TEST_SUITE_END(); public: @@ -547,6 +551,83 @@ public: CPPUNIT_ASSERT_EQUAL(TagLib::uint(86414), frame->picture().size()); } + void testPropertyInterface() + { + ScopedFileCopy copy("rare_frames", ".mp3"); + string newname = copy.fileName(); + MPEG::File f(newname.c_str()); + PropertyMap dict = f.ID3v2Tag(false)->properties(); + CPPUNIT_ASSERT_EQUAL(uint(6), dict.size()); + + CPPUNIT_ASSERT(dict.contains("USERTEXTDESCRIPTION1")); + CPPUNIT_ASSERT(dict.contains("QuodLibet::USERTEXTDESCRIPTION2")); + CPPUNIT_ASSERT_EQUAL(uint(2), dict["USERTEXTDESCRIPTION1"].size()); + CPPUNIT_ASSERT_EQUAL(uint(2), dict["QuodLibet::USERTEXTDESCRIPTION2"].size()); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["QuodLibet::USERTEXTDESCRIPTION2"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["QuodLibet::USERTEXTDESCRIPTION2"][1]); + + CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"].front()); + + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url"), dict["URL:USERURL"].front()); + + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url/with/empty/description"), dict["URL"].front()); + CPPUNIT_ASSERT_EQUAL(String("A COMMENT"), dict["COMMENT"].front()); + + CPPUNIT_ASSERT_EQUAL(1u, dict.unsupportedData().size()); + CPPUNIT_ASSERT_EQUAL(String("UFID"), dict.unsupportedData().front()); + } + + void testPropertyInterface2() + { + ID3v2::Tag tag; + ID3v2::UnsynchronizedLyricsFrame *frame1 = new ID3v2::UnsynchronizedLyricsFrame(); + frame1->setDescription("test"); + frame1->setText("la-la-la test"); + tag.addFrame(frame1); + + ID3v2::UnsynchronizedLyricsFrame *frame2 = new ID3v2::UnsynchronizedLyricsFrame(); + frame2->setDescription(""); + frame2->setText("la-la-la nodescription"); + tag.addFrame(frame2); + + ID3v2::AttachedPictureFrame *frame3 = new ID3v2::AttachedPictureFrame(); + frame3->setDescription("test picture"); + tag.addFrame(frame3); + + ID3v2::TextIdentificationFrame *frame4 = new ID3v2::TextIdentificationFrame("TIPL"); + frame4->setText("single value is invalid for TIPL"); + tag.addFrame(frame4); + + ID3v2::TextIdentificationFrame *frame5 = new ID3v2::TextIdentificationFrame("TMCL"); + StringList tmclData; + tmclData.append("VIOLIN"); + tmclData.append("a violinist"); + tmclData.append("PIANO"); + tmclData.append("a pianist"); + frame5->setText(tmclData); + tag.addFrame(frame5); + + PropertyMap properties = tag.properties(); + + CPPUNIT_ASSERT_EQUAL(2u, properties.unsupportedData().size()); + CPPUNIT_ASSERT(properties.unsupportedData().contains("TIPL")); + CPPUNIT_ASSERT(properties.unsupportedData().contains("APIC")); + + CPPUNIT_ASSERT(properties.contains("PERFORMER:VIOLIN")); + CPPUNIT_ASSERT(properties.contains("PERFORMER:PIANO")); + CPPUNIT_ASSERT_EQUAL(String("a violinist"), properties["PERFORMER:VIOLIN"].front()); + CPPUNIT_ASSERT_EQUAL(String("a pianist"), properties["PERFORMER:PIANO"].front()); + + CPPUNIT_ASSERT(properties.contains("LYRICS")); + CPPUNIT_ASSERT(properties.contains("LYRICS:TEST")); + + tag.removeUnsupportedProperties(properties.unsupportedData()); + CPPUNIT_ASSERT(tag.frameList("APIC").isEmpty()); + CPPUNIT_ASSERT(tag.frameList("TIPL").isEmpty()); + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2); diff --git a/tests/test_mod.cpp b/tests/test_mod.cpp index 67c46f28..a3919d7e 100644 --- a/tests/test_mod.cpp +++ b/tests/test_mod.cpp @@ -21,6 +21,7 @@ #include #include +#include #include "utils.h" using namespace std; @@ -51,6 +52,7 @@ class TestMod : public CppUnit::TestFixture CPPUNIT_TEST_SUITE(TestMod); CPPUNIT_TEST(testReadTags); CPPUNIT_TEST(testWriteTags); + CPPUNIT_TEST(testPropertyInterface); CPPUNIT_TEST_SUITE_END(); public: @@ -75,6 +77,22 @@ public: TEST_FILE_PATH_C("changed.mod"))); } + void testPropertyInterface() + { + Mod::Tag t; + PropertyMap properties; + properties["BLA"] = String("bla"); + properties["ARTIST"] = String("artist1"); + properties["ARTIST"].append("artist2"); + properties["TITLE"] = String("title"); + + PropertyMap unsupported = t.setProperties(properties); + CPPUNIT_ASSERT(unsupported.contains("BLA")); + CPPUNIT_ASSERT(unsupported.contains("ARTIST")); + CPPUNIT_ASSERT_EQUAL(properties["ARTIST"], unsupported["ARTIST"]); + CPPUNIT_ASSERT(!unsupported.contains("TITLE")); + } + private: void testRead(FileName fileName, const String &title, const String &comment) { diff --git a/tests/test_ogg.cpp b/tests/test_ogg.cpp index 9e845096..b5c6b557 100644 --- a/tests/test_ogg.cpp +++ b/tests/test_ogg.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,8 @@ class TestOGG : public CppUnit::TestFixture CPPUNIT_TEST_SUITE(TestOGG); CPPUNIT_TEST(testSimple); CPPUNIT_TEST(testSplitPackets); + CPPUNIT_TEST(testDictInterface1); + CPPUNIT_TEST(testDictInterface2); CPPUNIT_TEST_SUITE_END(); public: @@ -51,6 +54,51 @@ public: delete f; } + void testDictInterface1() + { + ScopedFileCopy copy("empty", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + + CPPUNIT_ASSERT_EQUAL(uint(0), f->tag()->properties().size()); + + PropertyMap newTags; + StringList values("value 1"); + values.append("value 2"); + newTags["ARTIST"] = values; + f->tag()->setProperties(newTags); + + PropertyMap map = f->tag()->properties(); + CPPUNIT_ASSERT_EQUAL(uint(1), map.size()); + CPPUNIT_ASSERT_EQUAL(uint(2), map["ARTIST"].size()); + CPPUNIT_ASSERT_EQUAL(String("value 1"), map["ARTIST"][0]); + delete f; + + } + + void testDictInterface2() + { + ScopedFileCopy copy("test", ".ogg"); + string newname = copy.fileName(); + + Vorbis::File *f = new Vorbis::File(newname.c_str()); + PropertyMap tags = f->tag()->properties(); + + CPPUNIT_ASSERT_EQUAL(uint(2), tags["UNUSUALTAG"].size()); + CPPUNIT_ASSERT_EQUAL(String("usual value"), tags["UNUSUALTAG"][0]); + CPPUNIT_ASSERT_EQUAL(String("another value"), tags["UNUSUALTAG"][1]); + CPPUNIT_ASSERT_EQUAL(String(L"öäüoΣø"), tags["UNICODETAG"][0]); + + tags["UNICODETAG"][0] = L"νεω ναλυε"; + tags.erase("UNUSUALTAG"); + f->tag()->setProperties(tags); + CPPUNIT_ASSERT_EQUAL(String(L"νεω ναλυε"), f->tag()->properties()["UNICODETAG"][0]); + CPPUNIT_ASSERT_EQUAL(false, f->tag()->properties().contains("UNUSUALTAG")); + + delete f; + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestOGG);