/*!
* 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"},
// 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
"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
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())
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);
***************************************************************************/
#include "tag.h"
-
+#include "tstringlist.h"
using namespace TagLib;
class Tag::TagPrivate
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) {