]> granicus.if.org Git - taglib/commitdiff
More support for the unified dictionary interface.
authorMichael Helmling <helmling@mathematik.uni-kl.de>
Fri, 26 Aug 2011 23:18:21 +0000 (01:18 +0200)
committerMichael Helmling <helmling@mathematik.uni-kl.de>
Fri, 26 Aug 2011 23:18:21 +0000 (01:18 +0200)
Addded fromDict() function to ID3v2Tag. Added fromDict() and
toDict() functions to the TagUnion class (uses the first non-empty tag).
Added fromDict() and toDict() functions for the generic Tag class, only
handling common tags without duplicates. Addded preliminary mp3 test
case. Python3 bindings now available on my github site.

taglib/mpeg/id3v2/id3v2dicttools.cpp
taglib/mpeg/id3v2/id3v2dicttools.h
taglib/mpeg/id3v2/id3v2tag.cpp
taglib/mpeg/id3v2/id3v2tag.h
taglib/ogg/xiphcomment.h
taglib/tag.cpp
taglib/tag.h
taglib/tagunion.cpp
taglib/tagunion.h
tests/data/rare_frames.mp3 [new file with mode: 0644]
tests/test_id3v2.cpp

index 87e16ed4fdedf0f94ef7a6eb75dfdd60499ddaa8..903c93724d705536670da6123a2868ab39ff710b 100644 (file)
@@ -31,7 +31,7 @@ namespace TagLib {
     /*!
      * A map of translations frameID <-> tag used by the unified dictionary interface.
      */
-    static const uint numid3frames = 55;
+    static const uint numid3frames = 54;
     static const char *id3frames[][2] = {
       // Text information frames
       { "TALB", "ALBUM"},
@@ -96,11 +96,10 @@ namespace TagLib {
       // 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 uint ignoredFramesSize = 7;
     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
@@ -108,6 +107,7 @@ namespace TagLib {
       "APIC", // attached picture -- TODO how could we do this?
       "POPM", // popularimeter
       "RVA2", // relative volume
+      "UFID", // unique file identifier
     };
 
     // list of deprecated frames and their successors
@@ -133,6 +133,16 @@ namespace TagLib {
       return "UNKNOWNID3TAG"; //TODO: implement this nicer
     }
 
+    ByteVector tagNameToFrameID(const String &s) {
+      static Map<String, ByteVector> m;
+      if (m.isEmpty())
+        for (size_t i = 0; i < numid3frames; ++i)
+          m[id3frames[i][1]] = id3frames[i][0];
+      if (m.contains(s.upper()))
+        return m[s];
+      return "TXXX";
+    }
+
     bool isIgnored(const ByteVector& id) {
       List<ByteVector> ignoredList;
       if (ignoredList.isEmpty())
index a6209e7e750965a87e5c7a4d9a2a2a15b9f6a66b..3e7a329fdd60357d055c77f6f9e3462a22f9f304 100644 (file)
@@ -38,7 +38,9 @@ namespace TagLib {
      */
     typedef Map<ByteVector, ByteVector> FrameIDMap;
 
-    String TAGLIB_EXPORT frameIDToTagName(const ByteVector &id);
+    ByteVector TAGLIB_EXPORT tagNameToFrameID(const String &);
+
+    String TAGLIB_EXPORT frameIDToTagName(const ByteVector &);
 
     bool TAGLIB_EXPORT isIgnored(const ByteVector &);
 
index 8f74686573193971a4f5b87819b0dec9a606b232..83406cdb1e6f58fb8de2a723501712a472d1b948 100644 (file)
@@ -425,20 +425,197 @@ TagDict ID3v2::Tag::toDict() const
       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;
 }
 
+void ID3v2::Tag::fromDict(const TagDict &dict)
+{
+  FrameList toRemove;
+  // first record what frames to remove; we do not remove in-place
+  // because that would invalidate FrameListMap iterators.
+  //
+  for (FrameListMap::ConstIterator it = frameListMap().begin(); it != frameListMap().end(); ++it) {
+    if (it->second.size() == 0) // ignore empty map entries (does this ever happen?)
+        continue;
+    if (isDeprecated(it->first))// automatically remove deprecated frames
+        toRemove.append(it->second);
+    else if (it->first == "TXXX") { // handle user text frames specially
+      for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
+        UserTextIdentificationFrame* frame
+            = dynamic_cast< UserTextIdentificationFrame* >(*fit);
+        String tagName = frame->description();
+        int pos = tagName.find("::");
+        tagName = (pos == -1) ? tagName : tagName.substr(pos+2);
+        if (!dict.contains(tagName.upper()))
+          toRemove.append(frame);
+      }
+    }
+    else if (it->first == "WXXX") { // handle user URL frames specially
+      for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
+        UserUrlLinkFrame* frame = dynamic_cast<ID3v2::UserUrlLinkFrame* >(*fit);
+        String tagName = frame->description().upper();
+        if (!(tagName == "URL") || !dict.contains("URL") || dict["URL"].size() > 1)
+          toRemove.append(frame);
+      }
+    }
+    else if (it->first == "COMM") {
+      for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
+        CommentsFrame* frame = dynamic_cast< CommentsFrame* >(*fit);
+        String tagName = frame->description().upper();
+        // policy: use comment frame only with empty description and only if a comment tag
+        // is present in the dictionary and only if there's no more than one comment
+        // (COMM is not specified for multiple values)
+        if ( !(tagName == "") || !dict.contains("COMMENT") || dict["COMMENT"].size() > 1)
+          toRemove.append(frame);
+      }
+    }
+    else if (it->first == "USLT") {
+        for (FrameList::ConstIterator fit = it->second.begin(); fit != it->second.end(); ++fit) {
+          UnsynchronizedLyricsFrame *frame
+            = dynamic_cast< UnsynchronizedLyricsFrame* >(*fit);
+          String tagName = frame->description().upper();
+          if ( !(tagName == "") || !dict.contains("LYRICS") || dict["LYRICS"].size() > 1)
+            toRemove.append(frame);
+        }
+    }
+    else if (it->first[0] == 'T') { // a normal text frame
+      if (!dict.contains(frameIDToTagName(it->first)))
+        toRemove.append(it->second);
+
+    } else
+      debug("file contains unknown tag" + it->first + ", not touching it...");
+  }
+
+  // now remove the frames that have been determined above
+  for (FrameList::ConstIterator it = toRemove.begin(); it != toRemove.end(); it++)
+    removeFrame(*it);
+
+  // now sync in the "forward direction"
+  for (TagDict::ConstIterator it = dict.begin(); it != dict.end(); ++it) {
+    const String &tagName = it->first;
+    ByteVector id = tagNameToFrameID(tagName);
+    if (id[0] == 'T' && id != "TXXX") {
+      // the easiest case: a normal text frame
+      StringList values = it->second;
+      const FrameList &framelist = frameList(id);
+      if (tagName == "DATE") {
+        // Handle ISO8601 date format (see above)
+        for (StringList::Iterator lit = values.begin(); lit != values.end();  ++lit) {
+          if (lit->length() > 10 && (*lit)[10] == ' ')
+            (*lit)[10] = 'T';
+        }
+      }
+      if (framelist.size() > 0) { // there exists already a frame for this tag
+        const TextIdentificationFrame *frame = dynamic_cast<const TextIdentificationFrame *>(framelist[0]);
+        if (values == frame->fieldList())
+          continue; // equal tag values -> everything ok
+      }
+      // if there was no frame for this tag, or there was one but the values aren't equal,
+      // we start from scratch and create a new one
+      //
+      removeFrames(id);
+      TextIdentificationFrame *frame = new TextIdentificationFrame(id);
+      frame->setText(values);
+      addFrame(frame);
+    }
+    else if (id == "TXXX" ||
+             ((id == "WXXX" || id == "COMM" || id == "USLT") && it->second.size() > 1)) {
+      // In all those cases, we store the tag as TXXX frame.
+      // First we search for existing TXXX frames with correct description
+      FrameList existingFrames;
+      FrameList l = frameList("TXXX");
+
+      for (FrameList::ConstIterator fit = l.begin(); fit != l.end(); fit++) {
+        String desc= dynamic_cast< UserTextIdentificationFrame* >(*fit)->description();
+        int pos = desc.find("::");
+        String tagName = (pos == -1) ? desc.upper() : desc.substr(pos+2).upper();
+        if (tagName == it->first)
+          existingFrames.append(*fit);
+      }
+
+      bool needsInsert = false;
+      if (existingFrames.size() > 1) { //several tags with same key, remove all and reinsert
+        for (FrameList::ConstIterator it = existingFrames.begin(); it != existingFrames.end(); ++it)
+          removeFrame(*it);
+        needsInsert = true;
+      }
+      else if (existingFrames.isEmpty()) // no frame -> needs insert
+        needsInsert = true;
+      else {
+        if (!(dynamic_cast< UserTextIdentificationFrame*>(existingFrames[0])->fieldList() == it->second)) {
+          needsInsert = true;
+          removeFrame(existingFrames[0]);
+        }
+      }
+      if (needsInsert) { // create and insert new frame
+        UserTextIdentificationFrame* frame = new UserTextIdentificationFrame();
+        frame->setDescription(it->first);
+        frame->setText(it->second);
+        addFrame(frame);
+      }
+    }
+    else if (id == "WXXX") {
+      // we know that it->second.size()==1, since the other cases are handled above
+      bool needsInsert = true;
+      FrameList existingFrames = frameList(id);
+      if (existingFrames.size() > 1 ) // do not allow several WXXX frames
+        removeFrames(id);
+      else if (existingFrames.size() == 1) {
+        needsInsert = !(dynamic_cast< UserUrlLinkFrame* >(existingFrames[0])->url() == it->second[0]);
+        if (needsInsert)
+          removeFrames(id);
+      }
+      if (needsInsert) {
+        UserUrlLinkFrame* frame = new ID3v2::UserUrlLinkFrame();
+        frame->setDescription(it->first);
+        frame->setUrl(it->second[0]);
+        addFrame(frame);
+      }
+    }
+    else if (id == "COMM") {
+      FrameList existingFrames = frameList(id);
+      bool needsInsert = true;
+      if (existingFrames.size() > 1) // do not allow several COMM frames
+        removeFrames(id);
+      else if (existingFrames.size() == 1) {
+        needsInsert = !(dynamic_cast< CommentsFrame* >(existingFrames[0])->text() == it->second[0]);
+        if (needsInsert)
+          removeFrames(id);
+      }
+
+      if (needsInsert) {
+        CommentsFrame* frame = new CommentsFrame();
+        frame->setDescription(""); // most software players use empty description COMM frames for comments
+        frame->setText(it->second[0]);
+        addFrame(frame);
+      }
+    }
+    else if (id == "USLT") {
+      FrameList existingFrames = frameList(id);
+      bool needsInsert = true;
+      if (existingFrames.size() > 1) // do not allow several USLT frames
+          removeFrames(id);
+      else if (existingFrames.size() == 1) {
+          needsInsert = !(dynamic_cast< UnsynchronizedLyricsFrame* >(existingFrames[0])->text() == it->second[0]);
+          if (needsInsert)
+            removeFrames(id);
+      }
+
+      if (needsInsert) {
+        UnsynchronizedLyricsFrame* frame = new UnsynchronizedLyricsFrame();
+        frame->setDescription("");
+        frame->setText(it->second[0]);
+        addFrame(frame);
+      }
+    }
+    else
+      debug("ERROR: Don't know how to translate tag " + it->first + " to ID3v2!");
+
+  }
+}
+
 ByteVector ID3v2::Tag::render() const
 {
   return render(4);
index 26eab2eba0d66e37474dd17de3a6e222514d16cb..715daf04f79873f66ec976244d20999a1c427bfa 100644 (file)
@@ -263,12 +263,12 @@ namespace TagLib {
       /*!
        * Implements the unified tag dictionary interface -- export function.
        */
-      TagDict toDict() const;
+      virtual TagDict toDict() const;
 
       /*!
        * Implements the unified tag dictionary interface -- import function.
        */
-      void fromDict(const TagDict &);
+      virtual void fromDict(const TagDict &);
 
       /*!
        * Render the tag back to binary data, suitable to be written to disk.
index 9eb329b34a7297d250d5a5deb9f8d6a90794d486..988f616ddcecee9916d85a53bd41fd92771fc93c 100644 (file)
@@ -143,12 +143,12 @@ namespace TagLib {
       /*!
        * Implements the unified tag dictionary interface -- export function.
        */
-      TagDict toDict() const;
+      virtual TagDict toDict() const;
 
       /*!
        * Implements the unified tag dictionary interface -- import function.
        */
-      void fromDict(const TagDict &);
+      virtual void fromDict(const TagDict &);
 
       /*!
        * Returns the vendor ID of the Ogg Vorbis encoder.  libvorbis 1.0 as the
index 8be33c80f1167737f836d8688ebf086b0f5513af..9e0ea25862d8af1999f855e51e341a94375bcb89 100644 (file)
@@ -24,7 +24,7 @@
  ***************************************************************************/
 
 #include "tag.h"
-
+#include "tstringlist.h"
 using namespace TagLib;
 
 class Tag::TagPrivate
@@ -53,6 +53,75 @@ bool Tag::isEmpty() const
           track() == 0);
 }
 
+TagDict Tag::toDict() const
+{
+  TagDict dict;
+  if (!(title() == String::null))
+    dict["TITLE"].append(title());
+  if (!(artist() == String::null))
+    dict["ARTIST"].append(artist());
+  if (!(album() == String::null))
+    dict["ALBUM"].append(album());
+  if (!(comment() == String::null))
+    dict["COMMENT"].append(comment());
+  if (!(genre() == String::null))
+    dict["GENRE"].append(genre());
+  if (!(year() == 0))
+    dict["DATE"].append(String::number(year()));
+  if (!(track() == 0))
+    dict["TRACKNUMBER"].append(String::number(track()));
+  return dict;
+}
+
+void Tag::fromDict(const TagDict &dict)
+{
+  if (dict.contains("TITLE") and dict["TITLE"].size() >= 1)
+    setTitle(dict["TITLE"].front());
+  else
+    setTitle(String::null);
+
+  if (dict.contains("ARTIST") and dict["ARTIST"].size() >= 1)
+    setArtist(dict["ARTIST"].front());
+  else
+    setArtist(String::null);
+
+  if (dict.contains("ALBUM") and dict["ALBUM"].size() >= 1)
+      setAlbum(dict["ALBUM"].front());
+    else
+      setAlbum(String::null);
+
+  if (dict.contains("COMMENT") and dict["COMMENT"].size() >= 1)
+    setComment(dict["COMMENT"].front());
+  else
+    setComment(String::null);
+
+  if (dict.contains("GENRE") and dict["GENRE"].size() >=1)
+    setGenre(dict["GENRE"].front());
+  else
+    setGenre(String::null);
+
+  if (dict.contains("DATE") and dict["DATE"].size() >= 1) {
+    bool ok;
+    int date = dict["DATE"].front().toInt(&ok);
+    if (ok)
+      setYear(date);
+    else
+      setYear(0);
+  }
+  else
+    setYear(0);
+
+  if (dict.contains("TRACKNUMBER") and dict["TRACKNUMBER"].size() >= 1) {
+    bool ok;
+    int track = dict["TRACKNUMBER"].front().toInt(&ok);
+    if (ok)
+      setTrack(track);
+    else
+      setTrack(0);
+  }
+  else
+    setYear(0);
+}
 void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static
 {
   if(overwrite) {
index 528d25f86a2eee0f65467932814c0fd991c2ec28..45caf08337aa08a7f4d050a97766ee630290ba33 100644 (file)
@@ -58,6 +58,22 @@ namespace TagLib {
      */
     virtual ~Tag();
 
+    /*!
+     * Unified tag dictionary interface -- export function. Converts the tags
+     * of the specific metadata format into a "human-readable" map of strings
+     * to lists of strings, being as precise as possible.
+     */
+    virtual TagDict toDict() const;
+
+    /*!
+     * Unified tag dictionary interface -- import function. Converts a map
+     * of strings to stringslists into the specific metadata format. Note that
+     * not all formats can store arbitrary tags and values, so data might
+     * be lost by this operation. Especially the default implementation handles
+     * only single values of the default tags specified in this class.
+     */
+    virtual void fromDict(const TagDict &);
+
     /*!
      * Returns the track name; if no track name is present in the tag
      * String::null will be returned.
index 4a9978d003f0b9c126064091cda8b19d08145ca4..2ecdd6d996a58e8a74d5ae4332135a0fd96b864f 100644 (file)
@@ -24,6 +24,7 @@
  ***************************************************************************/
 
 #include "tagunion.h"
+#include "tstringlist.h"
 
 using namespace TagLib;
 
@@ -170,6 +171,21 @@ void TagUnion::setTrack(uint i)
 {
   setUnion(Track, i);
 }
+TagDict TagUnion::toDict() const
+{
+  for (int i = 0; i < 3; ++i)
+    if (d->tags[i])
+      return d->tags[i]->toDict();
+  TagDict dict;
+  return dict;
+}
+
+void TagUnion::fromDict(const TagDict &dict)
+{
+  for (int i = 0; i < 3; ++i)
+    if (d->tags[i])
+      d->tags[i]->fromDict(dict);
+}
 
 bool TagUnion::isEmpty() const
 {
index e94d523a3bd64575e49a189509a24611b5c947ad..20771fe8e09450808de0e7d34e880cc91c901161 100644 (file)
@@ -73,6 +73,9 @@ namespace TagLib {
     virtual void setTrack(uint i);
     virtual bool isEmpty() const;
 
+    virtual TagDict toDict() const;
+    virtual void fromDict(const TagDict &);
+
     template <class T> T *access(int index, bool create)
     {
       if(!create || tag(index))
diff --git a/tests/data/rare_frames.mp3 b/tests/data/rare_frames.mp3
new file mode 100644 (file)
index 0000000..e485337
Binary files /dev/null and b/tests/data/rare_frames.mp3 differ
index 3035ebd8accc82dd0a03119a2391d398722e1f3c..067bcd221df4371017b93746e8d4647d7ad94f49 100644 (file)
@@ -554,7 +554,7 @@ public:
     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(uint(6), 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]);
@@ -566,8 +566,6 @@ public:
     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]);
   }