diff --git a/taglib/mpeg/id3v2/id3v2dicttools.cpp b/taglib/mpeg/id3v2/id3v2dicttools.cpp index 87e16ed4..903c9372 100644 --- a/taglib/mpeg/id3v2/id3v2dicttools.cpp +++ b/taglib/mpeg/id3v2/id3v2dicttools.cpp @@ -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 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 ignoredList; if (ignoredList.isEmpty()) diff --git a/taglib/mpeg/id3v2/id3v2dicttools.h b/taglib/mpeg/id3v2/id3v2dicttools.h index a6209e7e..3e7a329f 100644 --- a/taglib/mpeg/id3v2/id3v2dicttools.h +++ b/taglib/mpeg/id3v2/id3v2dicttools.h @@ -38,7 +38,9 @@ namespace TagLib { */ typedef Map 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 &); diff --git a/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/mpeg/id3v2/id3v2tag.cpp index 8f746865..83406cdb 100644 --- a/taglib/mpeg/id3v2/id3v2tag.cpp +++ b/taglib/mpeg/id3v2/id3v2tag.cpp @@ -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(*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(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); diff --git a/taglib/mpeg/id3v2/id3v2tag.h b/taglib/mpeg/id3v2/id3v2tag.h index 26eab2eb..715daf04 100644 --- a/taglib/mpeg/id3v2/id3v2tag.h +++ b/taglib/mpeg/id3v2/id3v2tag.h @@ -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. diff --git a/taglib/ogg/xiphcomment.h b/taglib/ogg/xiphcomment.h index 9eb329b3..988f616d 100644 --- a/taglib/ogg/xiphcomment.h +++ b/taglib/ogg/xiphcomment.h @@ -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 diff --git a/taglib/tag.cpp b/taglib/tag.cpp index 8be33c80..9e0ea258 100644 --- a/taglib/tag.cpp +++ b/taglib/tag.cpp @@ -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) { diff --git a/taglib/tag.h b/taglib/tag.h index 528d25f8..45caf083 100644 --- a/taglib/tag.h +++ b/taglib/tag.h @@ -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. diff --git a/taglib/tagunion.cpp b/taglib/tagunion.cpp index 4a9978d0..2ecdd6d9 100644 --- a/taglib/tagunion.cpp +++ b/taglib/tagunion.cpp @@ -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 { diff --git a/taglib/tagunion.h b/taglib/tagunion.h index e94d523a..20771fe8 100644 --- a/taglib/tagunion.h +++ b/taglib/tagunion.h @@ -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 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 index 00000000..e485337f Binary files /dev/null and b/tests/data/rare_frames.mp3 differ diff --git a/tests/test_id3v2.cpp b/tests/test_id3v2.cpp index 3035ebd8..067bcd22 100644 --- a/tests/test_id3v2.cpp +++ b/tests/test_id3v2.cpp @@ -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]); }