diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index c41c1ea6..ec8a5d81 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -54,6 +54,7 @@ set(tag_HDRS mpeg/xingheader.h mpeg/id3v1/id3v1tag.h mpeg/id3v1/id3v1genres.h + mpeg/id3v2/id3v2dicttools.h mpeg/id3v2/id3v2extendedheader.h mpeg/id3v2/id3v2frame.h mpeg/id3v2/id3v2header.h @@ -137,6 +138,7 @@ set(id3v1_SRCS ) set(id3v2_SRCS + mpeg/id3v2/id3v2dicttools.cpp mpeg/id3v2/id3v2framefactory.cpp mpeg/id3v2/id3v2synchdata.cpp mpeg/id3v2/id3v2tag.cpp diff --git a/taglib/mpeg/id3v2/id3v2dicttools.cpp b/taglib/mpeg/id3v2/id3v2dicttools.cpp new file mode 100644 index 00000000..87e16ed4 --- /dev/null +++ b/taglib/mpeg/id3v2/id3v2dicttools.cpp @@ -0,0 +1,156 @@ +/*************************************************************************** + copyright : (C) 2011 by Michael Helmling + email : supermihi@web.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 * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ +#include "tdebug.h" +#include "id3v2dicttools.h" +#include "tmap.h" +namespace TagLib { + namespace ID3v2 { + + /*! + * A map of translations frameID <-> tag used by the unified dictionary interface. + */ + static const uint numid3frames = 55; + static const char *id3frames[][2] = { + // Text information frames + { "TALB", "ALBUM"}, + { "TBPM", "BPM" }, + { "TCOM", "COMPOSER" }, + { "TCON", "GENRE" }, + { "TCOP", "COPYRIGHT" }, + { "TDEN", "ENCODINGTIME" }, + { "TDLY", "PLAYLISTDELAY" }, + { "TDOR", "ORIGINALRELEASETIME" }, + { "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", "RELEASETIME" }, + { "TDTG", "TAGGINGTIME" }, + { "TENC", "ENCODEDBY" }, + { "TEXT", "LYRICIST" }, + { "TFLT", "FILETYPE" }, + { "TIPL", "INVOLVEDPEOPLE" }, + { "TIT1", "CONTENTGROUP" }, + { "TIT2", "TITLE"}, + { "TIT3", "SUBTITLE" }, + { "TKEY", "INITIALKEY" }, + { "TLAN", "LANGUAGE" }, + { "TLEN", "LENGTH" }, + { "TMCL", "MUSICIANCREDITS" }, + { "TMED", "MEDIATYPE" }, + { "TMOO", "MOOD" }, + { "TOAL", "ORIGINALALBUM" }, + { "TOFN", "ORIGINALFILENAME" }, + { "TOLY", "ORIGINALLYRICIST" }, + { "TOPE", "ORIGINALARTIST" }, + { "TOWN", "OWNER" }, + { "TPE1", "ARTIST"}, + { "TPE2", "PERFORMER" }, + { "TPE3", "CONDUCTOR" }, + { "TPE4", "ARRANGER" }, + { "TPOS", "DISCNUMBER" }, + { "TPRO", "PRODUCEDNOTICE" }, + { "TPUB", "PUBLISHER" }, + { "TRCK", "TRACKNUMBER" }, + { "TRSN", "RADIOSTATION" }, + { "TRSO", "RADIOSTATIONOWNER" }, + { "TSOA", "ALBUMSORT" }, + { "TSOP", "ARTISTSORT" }, + { "TSOT", "TITLESORT" }, + { "TSRC", "ISRC" }, + { "TSSE", "ENCODING" }, + + // URL frames + { "WCOP", "COPYRIGHTURL" }, + { "WOAF", "FILEWEBPAGE" }, + { "WOAR", "ARTISTWEBPAGE" }, + { "WOAS", "AUDIOSOURCEWEBPAGE" }, + { "WORS", "RADIOSTATIONWEBPAGE" }, + { "WPAY", "PAYMENTWEBPAGE" }, + { "WPUB", "PUBLISHERWEBPAGE" }, + { "WXXX", "URL"}, + + // Other frames + { "COMM", "COMMENT" }, + { "USLT", "LYRICS" }, + { "UFID", "UNIQUEIDENTIFIER" }, + }; + + // list of frameIDs that are ignored by the unified dictionary interface + static const uint ignoredFramesSize = 6; + static const char *ignoredFrames[] = { + "TCMP", // illegal 'Part of Compilation' frame set by iTunes (see http://www.id3.org/Compliance_Issues) + "GEOB", // no way to handle a general encapsulated object by the dict interface + "PRIV", // private frames + "APIC", // attached picture -- TODO how could we do this? + "POPM", // popularimeter + "RVA2", // relative volume + }; + + // 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 + }; + + String frameIDToTagName(const ByteVector &id) { + static Map m; + if (m.isEmpty()) + for (size_t i = 0; i < numid3frames; ++i) + m[id3frames[i][0]] = id3frames[i][1]; + + if (m.contains(id)) + return m[id]; + if (deprecationMap().contains(id)) + return m[deprecationMap()[id]]; + debug("unknown frame ID: " + id); + return "UNKNOWNID3TAG"; //TODO: implement this nicer + } + + bool isIgnored(const ByteVector& id) { + List ignoredList; + if (ignoredList.isEmpty()) + for (uint i = 0; i < ignoredFramesSize; ++i) + ignoredList.append(ignoredFrames[i]); + return ignoredList.contains(id); + } + + FrameIDMap deprecationMap() { + static FrameIDMap depMap; + if (depMap.isEmpty()) + for(uint i = 0; i < deprecatedFramesSize; ++i) + depMap[deprecatedFrames[i][0]] = deprecatedFrames[i][1]; + return depMap; + } + + bool isDeprecated(const ByteVector& id) { + return deprecationMap().contains(id); + } + } +} diff --git a/taglib/mpeg/id3v2/id3v2dicttools.h b/taglib/mpeg/id3v2/id3v2dicttools.h new file mode 100644 index 00000000..a6209e7e --- /dev/null +++ b/taglib/mpeg/id3v2/id3v2dicttools.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2011 by Michael Helmling + email : supermihi@web.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 * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef ID3V2DICTTOOLS_H_ +#define ID3V2DICTTOOLS_H_ + +#include "tstringlist.h" +#include "taglib_export.h" +#include "tmap.h" + +namespace TagLib { + namespace ID3v2 { + /*! + * This file contains methods used by the unified dictionary interface for ID3v2 tags + * (tag name conversion, handling of un-translatable frameIDs, ...). + */ + typedef Map FrameIDMap; + + String TAGLIB_EXPORT frameIDToTagName(const ByteVector &id); + + bool TAGLIB_EXPORT isIgnored(const ByteVector &); + + FrameIDMap TAGLIB_EXPORT deprecationMap(); + + bool TAGLIB_EXPORT isDeprecated(const ByteVector&); + + + } +} + + +#endif /* ID3V2DICTTOOLS_H_ */ diff --git a/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/mpeg/id3v2/id3v2tag.cpp index 5b4c5c5b..8f746865 100644 --- a/taglib/mpeg/id3v2/id3v2tag.cpp +++ b/taglib/mpeg/id3v2/id3v2tag.cpp @@ -31,11 +31,15 @@ #include "id3v2extendedheader.h" #include "id3v2footer.h" #include "id3v2synchdata.h" - +#include "id3v2dicttools.h" +#include "tbytevector.h" #include "id3v1genres.h" #include "frames/textidentificationframe.h" #include "frames/commentsframe.h" +#include "frames/urllinkframe.h" +#include "frames/uniquefileidentifierframe.h" +#include "frames/unsynchronizedlyricsframe.h" using namespace TagLib; using namespace ID3v2; @@ -324,9 +328,115 @@ 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); +} + +TagDict ID3v2::Tag::toDict() const +{ + TagDict dict; + FrameList::ConstIterator frameIt = frameList().begin(); + for (; frameIt != frameList().end(); ++frameIt) { + ByteVector id = (*frameIt)->frameID(); + + if (isIgnored(id)) { + debug("found ignored id3 frame " + id); + continue; + } + if (isDeprecated(id)) { + debug("found deprecated id3 frame " + id); + continue; + } + if (id[0] == 'T') { + if (id == "TXXX") { + const UserTextIdentificationFrame *uframe + = dynamic_cast< const UserTextIdentificationFrame* >(*frameIt); + String tagName = uframe->description(); + StringList l(uframe->fieldList()); + // this is done because taglib stores the description also as first entry + // in the field list. (why?) + // + if (l.contains(tagName)) + l.erase(l.find(tagName)); + // handle user text frames set by the QuodLibet / exFalso package, + // which sets the description to QuodLibet:: instead of simply + // . + int pos = tagName.find("::"); + tagName = (pos != -1) ? tagName.substr(pos+2) : tagName; + dict[tagName.upper()].append(l); + } + else { + const TextIdentificationFrame* tframe + = dynamic_cast< const TextIdentificationFrame* >(*frameIt); + String tagName = frameIDToTagName(id); + StringList l = tframe->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 lit = l.begin(); lit != l.end(); ++lit) { + bool ok = false; + int test = lit->toInt(&ok); // test if the genre value is an integer + if (ok) { + *lit = ID3v1::genre(test); + } + } + } + else if (tagName == "DATE") { + for (StringList::Iterator lit = l.begin(); lit != l.end(); ++lit) { + // 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 = lit->find("T"); + if (tpos != -1) + (*lit)[tpos] = ' '; + } + } + dict[tagName].append(l); + } + continue; + } + if (id[0] == 'W') { + if (id == "WXXX") { + const UserUrlLinkFrame *uframe = dynamic_cast< const UserUrlLinkFrame* >(*frameIt); + String tagname = uframe->description().upper(); + if (tagname == "") + tagname = "URL"; + dict[tagname].append(uframe->url()); + } + else { + const UrlLinkFrame* uframe = dynamic_cast< const UrlLinkFrame* >(*frameIt); + dict[frameIDToTagName(id)].append(uframe->url()); + } + continue; + } + if (id == "COMM") { + const CommentsFrame *cframe = dynamic_cast< const CommentsFrame* >(*frameIt); + String tagName = cframe->description().upper(); + if (tagName.isEmpty()) + tagName = "COMMENT"; + dict[tagName].append(cframe->text()); + continue; + } + if (id == "USLT") { + const UnsynchronizedLyricsFrame *uframe + = dynamic_cast< const UnsynchronizedLyricsFrame* >(*frameIt); + dict["LYRICS"].append(uframe->text()); + continue; + } + if (id == "UFID") { + const UniqueFileIdentifierFrame *uframe + = dynamic_cast< const UniqueFileIdentifierFrame* >(*frameIt); + String value = uframe->identifier(); + if (!uframe->owner().isEmpty()) + value.append(" [" + uframe->owner() + "]"); + dict["UNIQUEIDENTIFIER"].append(value); + continue; + } + debug("unknown frame ID: " + id); + } + return dict; } ByteVector ID3v2::Tag::render() const diff --git a/taglib/mpeg/id3v2/id3v2tag.h b/taglib/mpeg/id3v2/id3v2tag.h index 4a52854a..26eab2eb 100644 --- a/taglib/mpeg/id3v2/id3v2tag.h +++ b/taglib/mpeg/id3v2/id3v2tag.h @@ -260,6 +260,16 @@ namespace TagLib { */ void removeFrames(const ByteVector &id); + /*! + * Implements the unified tag dictionary interface -- export function. + */ + TagDict toDict() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + */ + void fromDict(const TagDict &); + /*! * Render the tag back to binary data, suitable to be written to disk. */ diff --git a/taglib/ogg/xiphcomment.cpp b/taglib/ogg/xiphcomment.cpp index c26391a9..1d083a71 100644 --- a/taglib/ogg/xiphcomment.cpp +++ b/taglib/ogg/xiphcomment.cpp @@ -188,6 +188,47 @@ const Ogg::FieldListMap &Ogg::XiphComment::fieldListMap() const return d->fieldListMap; } +TagDict Ogg::XiphComment::toDict() const +{ + return d->fieldListMap; +} + +void Ogg::XiphComment::fromDict(const TagDict &tagDict) +{ + // check which keys are to be deleted + StringList toRemove; + FieldListMap::ConstIterator it = d->fieldListMap.begin(); + for(; it != d->fieldListMap.end(); ++it) { + if (!tagDict.contains(it->first)) + toRemove.append(it->first); + } + + StringList::ConstIterator removeIt = toRemove.begin(); + for (; removeIt != toRemove.end(); ++removeIt) + removeField(*removeIt); + + /* now go through keys in tagDict and check that the values match those in the xiph comment */ + TagDict::ConstIterator tagIt = tagDict.begin(); + for (; tagIt != tagDict.end(); ++tagIt) + { + if (!d->fieldListMap.contains(tagIt->first) || !(tagIt->second == d->fieldListMap[tagIt->first])) { + const StringList &sl = tagIt->second; + if(sl.size() == 0) { + // zero size string list -> remove the tag with all values + removeField(tagIt->first); + } + else { + // replace all strings in the list for the tag + StringList::ConstIterator valueIterator = sl.begin(); + addField(tagIt->first, *valueIterator, true); + ++valueIterator; + for(; valueIterator != sl.end(); ++valueIterator) + addField(tagIt->first, *valueIterator, false); + } + } + } +} + String Ogg::XiphComment::vendorID() const { return d->vendorID; diff --git a/taglib/ogg/xiphcomment.h b/taglib/ogg/xiphcomment.h index b105dd6a..9eb329b3 100644 --- a/taglib/ogg/xiphcomment.h +++ b/taglib/ogg/xiphcomment.h @@ -140,6 +140,16 @@ namespace TagLib { */ const FieldListMap &fieldListMap() const; + /*! + * Implements the unified tag dictionary interface -- export function. + */ + TagDict toDict() const; + + /*! + * Implements the unified tag dictionary interface -- import function. + */ + void fromDict(const TagDict &); + /*! * 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/tag.h b/taglib/tag.h index c8f12a85..528d25f8 100644 --- a/taglib/tag.h +++ b/taglib/tag.h @@ -28,9 +28,17 @@ #include "taglib_export.h" #include "tstring.h" +#include "tmap.h" namespace TagLib { + /*! + * This is used for the unified dictionary interface: the tags of a file are + * represented as a dictionary mapping a string (the tag name) to a list of + * strings (the values). + */ + typedef Map TagDict; + //! A simple, generic interface to common audio meta data fields /*! 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_id3v2.cpp b/tests/test_id3v2.cpp index 5b2151ad..3035ebd8 100644 --- a/tests/test_id3v2.cpp +++ b/tests/test_id3v2.cpp @@ -67,6 +67,7 @@ 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(testDictInterface); CPPUNIT_TEST_SUITE_END(); public: @@ -547,6 +548,29 @@ public: CPPUNIT_ASSERT_EQUAL(TagLib::uint(86414), frame->picture().size()); } + void testDictInterface() + { + ScopedFileCopy copy("rare_frames", ".mp3"); + string newname = copy.fileName(); + MPEG::File f(newname.c_str()); + TagDict dict = f.ID3v2Tag(false)->toDict(); + CPPUNIT_ASSERT_EQUAL(uint(7), dict.size()); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]); + CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION2"][0]); + CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION2"][1]); + + CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"][0]); + CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"][0]); + + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url"), dict["USERURL"][0]); + CPPUNIT_ASSERT_EQUAL(String("http://a.user.url/with/empty/description"), dict["URL"][0]); + + CPPUNIT_ASSERT_EQUAL(String("12345678 [supermihi@web.de]"), dict["UNIQUEIDENTIFIER"][0]); + + CPPUNIT_ASSERT_EQUAL(String("A COMMENT"), dict["COMMENT"][0]); + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2); diff --git a/tests/test_ogg.cpp b/tests/test_ogg.cpp index 9e845096..de25d3ed 100644 --- a/tests/test_ogg.cpp +++ b/tests/test_ogg.cpp @@ -17,6 +17,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 +53,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()->toDict().size()); + + TagDict newTags; + StringList values("value 1"); + values.append("value 2"); + newTags["ARTIST"] = values; + f->tag()->fromDict(newTags); + + TagDict map = f->tag()->toDict(); + 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()); + TagDict tags = f->tag()->toDict(); + + 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()->fromDict(tags); + CPPUNIT_ASSERT_EQUAL(String(L"νεω ναλυε"), f->tag()->toDict()["UNICODETAG"][0]); + CPPUNIT_ASSERT_EQUAL(false, f->tag()->toDict().contains("UNUSUALTAG")); + + delete f; + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestOGG);