]> granicus.if.org Git - taglib/commitdiff
Some preliminary work for unified dictionary tag interface support.
authorMichael Helmling <helmling@mathematik.uni-kl.de>
Fri, 26 Aug 2011 19:48:40 +0000 (21:48 +0200)
committerMichael Helmling <helmling@mathematik.uni-kl.de>
Fri, 26 Aug 2011 19:48:40 +0000 (21:48 +0200)
- toDict() and fromDict() for XiphComments
- toDict() for ID3v2 Tags

taglib/CMakeLists.txt
taglib/mpeg/id3v2/id3v2dicttools.cpp [new file with mode: 0644]
taglib/mpeg/id3v2/id3v2dicttools.h [new file with mode: 0644]
taglib/mpeg/id3v2/id3v2tag.cpp
taglib/mpeg/id3v2/id3v2tag.h
taglib/ogg/xiphcomment.cpp
taglib/ogg/xiphcomment.h
taglib/tag.h
tests/data/test.ogg [new file with mode: 0644]
tests/test_id3v2.cpp
tests/test_ogg.cpp

index c41c1ea606a1c8bf03339a41dc0afc929df18dda..ec8a5d81f8e771b5dabee0c6753f75960c2bd115 100644 (file)
@@ -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 (file)
index 0000000..87e16ed
--- /dev/null
@@ -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<ByteVector, String> 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<ByteVector> 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 (file)
index 0000000..a6209e7
--- /dev/null
@@ -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<ByteVector, ByteVector> 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_ */
index 5b4c5c5b335239731a87fbe610fd59fcc851cef0..8f74686573193971a4f5b87819b0dec9a606b232 100644 (file)
 #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::<tagName> instead of simply
+        // <tagName>.
+        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
index 4a52854afb0cb148a90d8ff2bb0125a06ba6357d..26eab2eba0d66e37474dd17de3a6e222514d16cb 100644 (file)
@@ -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.
        */
index c26391a91b2a366f74229e58a38d36343349301d..1d083a71b8373d4c2b2881cf2f67653ac07b8eca 100644 (file)
@@ -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;
index b105dd6a7a862a4065a0eacbefec7722c33975b2..9eb329b34a7297d250d5a5deb9f8d6a90794d486 100644 (file)
@@ -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".
index c8f12a853959085785aba9ae228faccc863beee0..528d25f86a2eee0f65467932814c0fd991c2ec28 100644 (file)
 
 #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<String, StringList> 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 (file)
index 0000000..220f76f
Binary files /dev/null and b/tests/data/test.ogg differ
index 5b2151add1eec5ff5b8a3b5eb18c4381e54742b3..3035ebd8accc82dd0a03119a2391d398722e1f3c 100644 (file)
@@ -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);
index 9e845096dc840f4424d36c6f7c23baf8b42dc10e..de25d3ed614bc4e07a7f08e45a4a36da0ed97987 100644 (file)
@@ -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);