TextIdentificationFrame *TextIdentificationFrame::createTIPLFrame(const PropertyMap &properties) // static { TextIdentificationFrame *frame = new TextIdentificationFrame("TIPL"); StringList l; for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ l.append(it->first); l.append(it->second.toString(",")); // comma-separated list of names } frame->setText(l); return frame; }
TextIdentificationFrame *TextIdentificationFrame::createTMCLFrame(const PropertyMap &properties) // static { TextIdentificationFrame *frame = new TextIdentificationFrame("TMCL"); StringList l; for(PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it){ if(!it->first.startsWith(instrumentPrefix)) // should not happen continue; l.append(it->first.substr(instrumentPrefix.size())); l.append(it->second.toString(",")); } frame->setText(l); return frame; }
String ID3v2::Tag::genre() const { // TODO: In the next major version (TagLib 2.0) a list of multiple genres // should be separated by " / " instead of " ". For the moment to keep // the behavior the same as released versions it is being left with " ". if(d->frameListMap["TCON"].isEmpty() || !dynamic_cast<TextIdentificationFrame *>(d->frameListMap["TCON"].front())) { return String::null; } // ID3v2.4 lists genres as the fields in its frames field list. If the field // is simply a number it can be assumed that it is an ID3v1 genre number. // Here was assume that if an ID3v1 string is present that it should be // appended to the genre string. Multiple fields will be appended as the // string is built. TextIdentificationFrame *f = static_cast<TextIdentificationFrame *>( d->frameListMap["TCON"].front()); StringList fields = f->fieldList(); StringList genres; for(StringList::Iterator it = fields.begin(); it != fields.end(); ++it) { if((*it).isEmpty()) continue; bool isNumber = true; for(String::ConstIterator charIt = (*it).begin(); isNumber && charIt != (*it).end(); ++charIt) { isNumber = *charIt >= '0' && *charIt <= '9'; } if(isNumber) { int number = (*it).toInt(); if(number >= 0 && number <= 255) *it = ID3v1::genre(number); } if(std::find(genres.begin(), genres.end(), *it) == genres.end()) genres.append(*it); } return genres.toString(); }
void ID3v2::Tag::setTextFrame(const ByteVector &id, const String &value) { if(value.isEmpty()) { removeFrames(id); return; } if(!d->frameListMap[id].isEmpty()) d->frameListMap[id].front()->setText(value); else { const String::Type encoding = d->factory->defaultTextEncoding(); TextIdentificationFrame *f = new TextIdentificationFrame(id, encoding); addFrame(f); f->setText(value); } }
Frame *Frame::createTextualFrame(const String &key, const StringList &values) //static { // check if the key is contained in the key<=>frameID mapping ByteVector frameID = keyToFrameID(key); if(!frameID.isNull()) { if(frameID[0] == 'T'){ // text frame TextIdentificationFrame *frame = new TextIdentificationFrame(frameID, String::UTF8); frame->setText(values); return frame; } else if((frameID[0] == 'W') && (values.size() == 1)){ // URL frame (not WXXX); support only one value UrlLinkFrame* frame = new UrlLinkFrame(frameID); frame->setUrl(values.front()); return frame; } } if(key == "MUSICBRAINZ_TRACKID" && values.size() == 1) { UniqueFileIdentifierFrame *frame = new UniqueFileIdentifierFrame("http://musicbrainz.org", values.front().data(String::UTF8)); return frame; } // now we check if it's one of the "special" cases: // -LYRICS: depending on the number of values, use USLT or TXXX (with description=LYRICS) if((key == "LYRICS" || key.startsWith(lyricsPrefix)) && values.size() == 1){ UnsynchronizedLyricsFrame *frame = new UnsynchronizedLyricsFrame(String::UTF8); frame->setDescription(key == "LYRICS" ? key : key.substr(lyricsPrefix.size())); frame->setText(values.front()); return frame; } // -URL: depending on the number of values, use WXXX or TXXX (with description=URL) if((key == "URL" || key.startsWith(urlPrefix)) && values.size() == 1){ UserUrlLinkFrame *frame = new UserUrlLinkFrame(String::UTF8); frame->setDescription(key == "URL" ? key : key.substr(urlPrefix.size())); frame->setUrl(values.front()); return frame; } // -COMMENT: depending on the number of values, use COMM or TXXX (with description=COMMENT) if((key == "COMMENT" || key.startsWith(commentPrefix)) && values.size() == 1){ CommentsFrame *frame = new CommentsFrame(String::UTF8); if (key != "COMMENT"){ frame->setDescription(key.substr(commentPrefix.size())); } frame->setText(values.front()); return frame; } // if non of the above cases apply, we use a TXXX frame with the key as description return new UserTextIdentificationFrame(keyToTXXX(key), values, String::UTF8); }
void FrameFactory::rebuildAggregateFrames(ID3v2::Tag *tag) const { if(tag->header()->majorVersion() < 4 && tag->frameList("TDRC").size() == 1 && tag->frameList("TDAT").size() == 1) { TextIdentificationFrame *tdrc = static_cast<TextIdentificationFrame *>(tag->frameList("TDRC").front()); UnknownFrame *tdat = static_cast<UnknownFrame *>(tag->frameList("TDAT").front()); if(tdrc->fieldList().size() == 1 && tdrc->fieldList().front().size() == 4 && tdat->data().size() >= 5) { String date(tdat->data().mid(1), String::Type(tdat->data()[0])); if(date.length() == 4) { tdrc->setText(tdrc->toString() + '-' + date.substr(2, 2) + '-' + date.substr(0, 2)); if(tag->frameList("TIME").size() == 1) { UnknownFrame *timeframe = static_cast<UnknownFrame *>(tag->frameList("TIME").front()); if(timeframe->data().size() >= 5) { String time(timeframe->data().mid(1), String::Type(timeframe->data()[0])); if(time.length() == 4) { tdrc->setText(tdrc->toString() + 'T' + time.substr(0, 2) + ':' + time.substr(2, 2)); } } } } } } }
Frame *FrameFactory::createFrame(const ByteVector &data, uint version) const { Frame::Header *header = new Frame::Header(data, version); ByteVector frameID = header->frameID(); // A quick sanity check -- make sure that the frameID is 4 uppercase Latin1 // characters. Also make sure that there is data in the frame. if(!frameID.size() == (version < 3 ? 3 : 4) || header->frameSize() <= 0 || header->frameSize() > data.size()) { delete header; return 0; } for(ByteVector::ConstIterator it = frameID.begin(); it != frameID.end(); it++) { if( (*it < 'A' || *it > 'Z') && (*it < '1' || *it > '9') ) { delete header; return 0; } } // TagLib doesn't mess with encrypted frames, so just treat them // as unknown frames. #if HAVE_ZLIB == 0 if(header->compression()) { debug("Compressed frames are currently not supported."); return new UnknownFrame(data, header); } #endif if(header->encryption()) { debug("Encrypted frames are currently not supported."); return new UnknownFrame(data, header); } if(!updateFrame(header)) { header->setTagAlterPreservation(true); return new UnknownFrame(data, header); } // updateFrame() might have updated the frame ID. frameID = header->frameID(); // This is where things get necissarily nasty. Here we determine which // Frame subclass (or if none is found simply an Frame) based // on the frame ID. Since there are a lot of possibilities, that means // a lot of if blocks. // Text Identification (frames 4.2) if(frameID.startsWith("T")) { TextIdentificationFrame *f = frameID != "TXXX" ? new TextIdentificationFrame(data, header) : new UserTextIdentificationFrame(data, header); if(d->useDefaultEncoding) f->setTextEncoding(d->defaultEncoding); if(frameID == "TCON") updateGenre(f); return f; } // Comments (frames 4.10) if(frameID == "COMM") { CommentsFrame *f = new CommentsFrame(data, header); if(d->useDefaultEncoding) f->setTextEncoding(d->defaultEncoding); return f; } // Attached Picture (frames 4.14) if(frameID == "APIC") { AttachedPictureFrame *f = new AttachedPictureFrame(data, header); if(d->useDefaultEncoding) f->setTextEncoding(d->defaultEncoding); return f; } // Relative Volume Adjustment (frames 4.11) if(frameID == "RVA2") return new RelativeVolumeFrame(data, header); // Unique File Identifier (frames 4.1) if(frameID == "UFID") return new UniqueFileIdentifierFrame(data, header); // General Encapsulated Object (frames 4.15) if(frameID == "GEOB") return new GeneralEncapsulatedObjectFrame(data, header); return new UnknownFrame(data, header); }
/*! * \copydoc MetaIO::read() */ MusicMetadata *MetaIOID3::read(const QString &filename) { if (!OpenFile(filename)) return nullptr; TagLib::ID3v2::Tag *tag = GetID3v2Tag(true); // Create tag if none are found // if there is no ID3v2 tag, try to read the ID3v1 tag and copy it to // the ID3v2 tag structure if (tag->isEmpty()) { TagLib::ID3v1::Tag *tag_v1 = GetID3v1Tag(); if (!tag_v1) return nullptr; if (!tag_v1->isEmpty()) { tag->setTitle(tag_v1->title()); tag->setArtist(tag_v1->artist()); tag->setAlbum(tag_v1->album()); tag->setTrack(tag_v1->track()); tag->setYear(tag_v1->year()); tag->setGenre(tag_v1->genre()); } } MusicMetadata *metadata = new MusicMetadata(filename); ReadGenericMetadata(tag, metadata); bool compilation = false; // Compilation Artist (TPE4 Remix) or fallback to (TPE2 Band) // N.B. The existance of a either frame is NOT an indication that this // is a compilation, but if it is then one of them will probably hold // the compilation artist. TextIdentificationFrame *tpeframe = nullptr; TagLib::ID3v2::FrameList tpelist = tag->frameListMap()["TPE4"]; if (tpelist.isEmpty() || tpelist.front()->toString().isEmpty()) tpelist = tag->frameListMap()["TPE2"]; if (!tpelist.isEmpty()) tpeframe = (TextIdentificationFrame *)tpelist.front(); if (tpeframe && !tpeframe->toString().isEmpty()) { QString compilation_artist = TStringToQString(tpeframe->toString()) .trimmed(); metadata->setCompilationArtist(compilation_artist); } // Rating and playcount, stored in POPM frame PopularimeterFrame *popm = findPOPM(tag, ""); // Global (all apps) tag // If no 'global' tag exists, look for the MythTV specific one if (!popm) { popm = findPOPM(tag, email); } // Fallback to using any POPM tag we can find if (!popm) { if (!tag->frameListMap()["POPM"].isEmpty()) popm = dynamic_cast<PopularimeterFrame *> (tag->frameListMap()["POPM"].front()); } if (popm) { int rating = popm->rating(); rating = lroundf(static_cast<float>(rating) / 255.0f * 10.0f); metadata->setRating(rating); metadata->setPlaycount(popm->counter()); } // Look for MusicBrainz Album+Artist ID in TXXX Frame UserTextIdentificationFrame *musicbrainz = find(tag, "MusicBrainz Album Artist Id"); if (musicbrainz) { // If the MusicBrainz ID is the special "Various Artists" ID // then compilation is TRUE if (!compilation && !musicbrainz->fieldList().isEmpty()) { TagLib::StringList l = musicbrainz->fieldList(); for (TagLib::StringList::ConstIterator it = l.begin(); it != l.end(); it++) { QString ID = TStringToQString((*it)); if (ID == MYTH_MUSICBRAINZ_ALBUMARTIST_UUID) { compilation = true; break; } } } } // TLEN - Ignored intentionally, some encoders write bad values // e.g. Lame under certain circumstances will always write a length of // 27 hours // Length if (!tag->frameListMap()["TLEN"].isEmpty()) { int length = tag->frameListMap()["TLEN"].front()->toString().toInt(); LOG(VB_FILE, LOG_DEBUG, QString("MetaIOID3::read: Length for '%1' from tag is '%2'\n").arg(filename).arg(length)); } metadata->setCompilation(compilation); metadata->setLength(getTrackLength(m_file)); // The number of tracks on the album, if supplied if (!tag->frameListMap()["TRCK"].isEmpty()) { QString trackFrame = TStringToQString( tag->frameListMap()["TRCK"].front()->toString()) .trimmed(); int trackCount = trackFrame.section('/', -1).toInt(); if (trackCount > 0) metadata->setTrackCount(trackCount); } LOG(VB_FILE, LOG_DEBUG, QString("MetaIOID3::read: Length for '%1' from properties is '%2'\n").arg(filename).arg(metadata->Length())); // Look for MythTVLastPlayed in TXXX Frame UserTextIdentificationFrame *lastplayed = find(tag, "MythTVLastPlayed"); if (lastplayed) { QString lastPlayStr = TStringToQString(lastplayed->toString()); metadata->setLastPlay(QDateTime::fromString(lastPlayStr, Qt::ISODate)); } // Part of a set if (!tag->frameListMap()["TPOS"].isEmpty()) { QString pos = TStringToQString( tag->frameListMap()["TPOS"].front()->toString()).trimmed(); int discNumber = pos.section('/', 0, 0).toInt(); int discCount = pos.section('/', -1).toInt(); if (discNumber > 0) metadata->setDiscNumber(discNumber); if (discCount > 0) metadata->setDiscCount(discCount); } return metadata; }
/*! * \copydoc MetaIO::read() */ Metadata *MetaIOID3::read(QString filename) { TagLib::MPEG::File *mpegfile = OpenFile(filename); if (!mpegfile) return NULL; TagLib::ID3v2::Tag *tag = mpegfile->ID3v2Tag(); if (!tag) { delete mpegfile; return NULL; } // if there is no ID3v2 tag, try to read the ID3v1 tag and copy it to the ID3v2 tag structure if (tag->isEmpty()) { TagLib::ID3v1::Tag *tag_v1 = mpegfile->ID3v1Tag(); if (!tag_v1) { delete mpegfile; return NULL; } if (!tag_v1->isEmpty()) { tag->setTitle(tag_v1->title()); tag->setArtist(tag_v1->artist()); tag->setAlbum(tag_v1->album()); tag->setTrack(tag_v1->track()); tag->setYear(tag_v1->year()); tag->setGenre(tag_v1->genre()); } } Metadata *metadata = new Metadata(filename); ReadGenericMetadata(tag, metadata); bool compilation = false; // Compilation Artist (TPE4 Remix) or fallback to (TPE2 Band) // N.B. The existance of a either frame is NOT an indication that this // is a compilation, but if it is then one of them will probably hold // the compilation artist. TextIdentificationFrame *tpeframe = NULL; TagLib::ID3v2::FrameList tpelist = tag->frameListMap()["TPE4"]; if (tpelist.isEmpty() || tpelist.front()->toString().isEmpty()) tpelist = tag->frameListMap()["TPE2"]; if (!tpelist.isEmpty()) tpeframe = (TextIdentificationFrame *)tpelist.front(); if (tpeframe && !tpeframe->toString().isEmpty()) { QString compilation_artist = TStringToQString(tpeframe->toString()) .trimmed(); metadata->setCompilationArtist(compilation_artist); } // MythTV rating and playcount, stored in POPM frame PopularimeterFrame *popm = findPOPM(tag, email); if (!popm) { if (!tag->frameListMap()["POPM"].isEmpty()) popm = dynamic_cast<PopularimeterFrame *> (tag->frameListMap()["POPM"].front()); } if (popm) { int rating = popm->rating(); rating = static_cast<int>(((static_cast<float>(rating)/255.0) * 10.0) + 0.5); metadata->setRating(rating); metadata->setPlaycount(popm->counter()); } // Look for MusicBrainz Album+Artist ID in TXXX Frame UserTextIdentificationFrame *musicbrainz = find(tag, "MusicBrainz Album Artist Id"); if (musicbrainz) { // If the MusicBrainz ID is the special "Various Artists" ID // then compilation is TRUE if (!compilation && !musicbrainz->fieldList().isEmpty()) compilation = (MYTH_MUSICBRAINZ_ALBUMARTIST_UUID == TStringToQString(musicbrainz->fieldList().front())); } // TLEN - Ignored intentionally, some encoders write bad values // e.g. Lame under certain circumstances will always write a length of // 27 hours metadata->setCompilation(compilation); TagLib::FileRef *fileref = new TagLib::FileRef(mpegfile); metadata->setLength(getTrackLength(fileref)); // FileRef takes ownership of mpegfile, and is responsible for it's // deletion. Messy. delete fileref; return metadata; }
/*! * \copydoc MetaIO::read() */ Metadata *MetaIOID3::read(const QString &filename) { if (!OpenFile(filename)) return NULL; TagLib::ID3v2::Tag *tag = GetID3v2Tag(true); // Create tag if none are found // if there is no ID3v2 tag, try to read the ID3v1 tag and copy it to // the ID3v2 tag structure if (tag->isEmpty()) { TagLib::ID3v1::Tag *tag_v1 = GetID3v1Tag(); if (!tag_v1) return NULL; if (!tag_v1->isEmpty()) { tag->setTitle(tag_v1->title()); tag->setArtist(tag_v1->artist()); tag->setAlbum(tag_v1->album()); tag->setTrack(tag_v1->track()); tag->setYear(tag_v1->year()); tag->setGenre(tag_v1->genre()); } } Metadata *metadata = new Metadata(filename); ReadGenericMetadata(tag, metadata); bool compilation = false; // Compilation Artist (TPE4 Remix) or fallback to (TPE2 Band) // N.B. The existance of a either frame is NOT an indication that this // is a compilation, but if it is then one of them will probably hold // the compilation artist. TextIdentificationFrame *tpeframe = NULL; TagLib::ID3v2::FrameList tpelist = tag->frameListMap()["TPE4"]; if (tpelist.isEmpty() || tpelist.front()->toString().isEmpty()) tpelist = tag->frameListMap()["TPE2"]; if (!tpelist.isEmpty()) tpeframe = (TextIdentificationFrame *)tpelist.front(); if (tpeframe && !tpeframe->toString().isEmpty()) { QString compilation_artist = TStringToQString(tpeframe->toString()) .trimmed(); metadata->setCompilationArtist(compilation_artist); } // MythTV rating and playcount, stored in POPM frame PopularimeterFrame *popm = findPOPM(tag, email); if (!popm) { if (!tag->frameListMap()["POPM"].isEmpty()) popm = dynamic_cast<PopularimeterFrame *> (tag->frameListMap()["POPM"].front()); } if (popm) { int rating = popm->rating(); rating = static_cast<int>(((static_cast<float>(rating)/255.0) * 10.0) + 0.5); metadata->setRating(rating); metadata->setPlaycount(popm->counter()); } // Look for MusicBrainz Album+Artist ID in TXXX Frame UserTextIdentificationFrame *musicbrainz = find(tag, "MusicBrainz Album Artist Id"); if (musicbrainz) { // If the MusicBrainz ID is the special "Various Artists" ID // then compilation is TRUE if (!compilation && !musicbrainz->fieldList().isEmpty()) compilation = (MYTH_MUSICBRAINZ_ALBUMARTIST_UUID == TStringToQString(musicbrainz->fieldList().front())); } // TLEN - Ignored intentionally, some encoders write bad values // e.g. Lame under certain circumstances will always write a length of // 27 hours // Length if (!tag->frameListMap()["TLEN"].isEmpty()) { int length = tag->frameListMap()["TLEN"].front()->toString().toInt(); LOG(VB_FILE, LOG_DEBUG, QString("MetaIOID3::read: Length for '%1' from tag is '%2'\n").arg(filename).arg(length)); } metadata->setCompilation(compilation); metadata->setLength(getTrackLength(m_file)); // The number of tracks on the album, if supplied if (!tag->frameListMap()["TRCK"].isEmpty()) { QString trackFrame = TStringToQString( tag->frameListMap()["TRCK"].front()->toString()) .trimmed(); int trackCount = trackFrame.section('/', -1).toInt(); if (trackCount > 0) metadata->setTrackCount(trackCount); } LOG(VB_FILE, LOG_DEBUG, QString("MetaIOID3::read: Length for '%1' from properties is '%2'\n").arg(filename).arg(metadata->Length())); return metadata; }
String ID3v2::Tag::genre() const { // TODO: In the next major version (TagLib 2.0) a list of multiple genres // should be separated by " / " instead of " ". For the moment to keep // the behavior the same as released versions it is being left with " ". if(!d->frameListMap["TCON"].isEmpty() && dynamic_cast<TextIdentificationFrame *>(d->frameListMap["TCON"].front())) { Frame *frame = d->frameListMap["TCON"].front(); // ID3v2.4 lists genres as the fields in its frames field list. If the field // is simply a number it can be assumed that it is an ID3v1 genre number. // Here was assume that if an ID3v1 string is present that it should be // appended to the genre string. Multiple fields will be appended as the // string is built. if(d->header.majorVersion() == 4) { TextIdentificationFrame *f = static_cast<TextIdentificationFrame *>(frame); StringList fields = f->fieldList(); String genreString; bool hasNumber = false; for(StringList::ConstIterator it = fields.begin(); it != fields.end(); ++it) { bool isNumber = true; for(String::ConstIterator charIt = (*it).begin(); isNumber && charIt != (*it).end(); ++charIt) { isNumber = *charIt >= '0' && *charIt <= '9'; } if(!genreString.isEmpty()) genreString.append(' '); if(isNumber) { int number = (*it).toInt(); if(number >= 0 && number <= 255) { hasNumber = true; genreString.append(ID3v1::genre(number)); } } else genreString.append(*it); } if(hasNumber) return genreString; } String s = frame->toString(); // ID3v2.3 "content type" can contain a ID3v1 genre number in parenthesis at // the beginning of the field. If this is all that the field contains, do a // translation from that number to the name and return that. If there is a // string folloing the ID3v1 genre number, that is considered to be // authoritative and we return that instead. Or finally, the field may // simply be free text, in which case we just return the value. int closing = s.find(")"); if(s.substr(0, 1) == "(" && closing > 0) { if(closing == int(s.size() - 1)) return ID3v1::genre(s.substr(1, s.size() - 2).toInt()); else return s.substr(closing + 1); } return s; } return String::null; }