bool SFB::Audio::AddID3v2TagToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::ID3v2::Tag *tag) { if(nullptr == dictionary || nullptr == tag) return false; // Add the basic tags not specific to ID3v2 AddTagToDictionary(dictionary, tag); // Release date auto frameList = tag->frameListMap()["TDRC"]; if(!frameList.isEmpty()) { /* The timestamp fields are based on a subset of ISO 8601. When being as precise as possible the format of a time string is yyyy-MM-ddTHH:mm:ss (year, "-", month, "-", day, "T", hour (out of 24), ":", minutes, ":", seconds), but the precision may be reduced by removing as many time indicators as wanted. Hence valid timestamps are yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use the slash character as described in 8601, and for multiple non- contiguous dates, use multiple strings, if allowed by the frame definition. */ TagLib::AddStringToCFDictionary(dictionary, Metadata::kReleaseDateKey, frameList.front()->toString()); } // Extract composer if present frameList = tag->frameListMap()["TCOM"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kComposerKey, frameList.front()->toString()); // Extract album artist frameList = tag->frameListMap()["TPE2"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumArtistKey, frameList.front()->toString()); // BPM frameList = tag->frameListMap()["TBPM"]; if(!frameList.isEmpty()) { bool ok = false; int BPM = frameList.front()->toString().toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kBPMKey, BPM); } // Rating TagLib::ID3v2::PopularimeterFrame *popularimeter = nullptr; frameList = tag->frameListMap()["POPM"]; if(!frameList.isEmpty() && nullptr != (popularimeter = dynamic_cast<TagLib::ID3v2::PopularimeterFrame *>(frameList.front()))) AddIntToDictionary(dictionary, Metadata::kRatingKey, popularimeter->rating()); // Extract total tracks if present frameList = tag->frameListMap()["TRCK"]; if(!frameList.isEmpty()) { // Split the tracks at '/' TagLib::String s = frameList.front()->toString(); bool ok; size_t pos = s.find("/", 0); if(TagLib::String::npos() != pos) { int trackNum = s.substr(0, pos).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, trackNum); int trackTotal = s.substr(pos + 1).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, trackTotal); } else if(s.length()) { int trackNum = s.toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, trackNum); } } // Extract disc number and total discs frameList = tag->frameListMap()["TPOS"]; if(!frameList.isEmpty()) { // Split the tracks at '/' TagLib::String s = frameList.front()->toString(); bool ok; size_t pos = s.find("/", 0); if(TagLib::String::npos() != pos) { int discNum = s.substr(0, pos).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, discNum); int discTotal = s.substr(pos + 1).toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, discTotal); } else if(s.length()) { int discNum = s.toInt(&ok); if(ok) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, discNum); } } // Lyrics frameList = tag->frameListMap()["USLT"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kLyricsKey, frameList.front()->toString()); // Extract compilation if present (iTunes TCMP tag) frameList = tag->frameListMap()["TCMP"]; if(!frameList.isEmpty()) // It seems that the presence of this frame indicates a compilation CFDictionarySetValue(dictionary, Metadata::kCompilationKey, kCFBooleanTrue); frameList = tag->frameListMap()["TSRC"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kISRCKey, frameList.front()->toString()); // MusicBrainz auto musicBrainzReleaseIDFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "MusicBrainz Album Id"); if(musicBrainzReleaseIDFrame) TagLib::AddStringToCFDictionary(dictionary, Metadata::kMusicBrainzReleaseIDKey, musicBrainzReleaseIDFrame->fieldList().back()); auto musicBrainzRecordingIDFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "MusicBrainz Track Id"); if(musicBrainzRecordingIDFrame) TagLib::AddStringToCFDictionary(dictionary, Metadata::kMusicBrainzRecordingIDKey, musicBrainzRecordingIDFrame->fieldList().back()); // Sorting and grouping frameList = tag->frameListMap()["TSOT"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kTitleSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOA"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumTitleSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOP"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kArtistSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSO2"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kAlbumArtistSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TSOC"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kComposerSortOrderKey, frameList.front()->toString()); frameList = tag->frameListMap()["TIT1"]; if(!frameList.isEmpty()) TagLib::AddStringToCFDictionary(dictionary, Metadata::kGroupingKey, frameList.front()->toString()); // ReplayGain bool foundReplayGain = false; // Preference is TXXX frames, RVA2 frame, then LAME header auto trackGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_TRACK_GAIN"); auto trackPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_TRACK_PEAK"); auto albumGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_ALBUM_GAIN"); auto albumPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "REPLAYGAIN_ALBUM_PEAK"); if(!trackGainFrame) trackGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_track_gain"); if(trackGainFrame) { SFB::CFString str(trackGainFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, num); AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, 89.0); foundReplayGain = true; } if(!trackPeakFrame) trackPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_track_peak"); if(trackPeakFrame) { SFB::CFString str(trackPeakFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, num); } if(!albumGainFrame) albumGainFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_album_gain"); if(albumGainFrame) { SFB::CFString str(albumGainFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, num); AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, 89.0); foundReplayGain = true; } if(!albumPeakFrame) albumPeakFrame = TagLib::ID3v2::UserTextIdentificationFrame::find(const_cast<TagLib::ID3v2::Tag *>(tag), "replaygain_album_peak"); if(albumPeakFrame) { SFB::CFString str(albumPeakFrame->fieldList().back().toCString(true), kCFStringEncodingUTF8); double num = CFStringGetDoubleValue(str); AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, num); } // If nothing found check for RVA2 frame if(!foundReplayGain) { frameList = tag->frameListMap()["RVA2"]; for(auto frameIterator : tag->frameListMap()["RVA2"]) { TagLib::ID3v2::RelativeVolumeFrame *relativeVolume = dynamic_cast<TagLib::ID3v2::RelativeVolumeFrame *>(frameIterator); if(!relativeVolume) continue; // Attempt to use the master volume if present auto channels = relativeVolume->channels(); auto channelType = TagLib::ID3v2::RelativeVolumeFrame::MasterVolume; // Fall back on whatever else exists in the frame if(!channels.contains(TagLib::ID3v2::RelativeVolumeFrame::MasterVolume)) channelType = channels.front(); float volumeAdjustment = relativeVolume->volumeAdjustment(channelType); if(TagLib::String("track", TagLib::String::Latin1) == relativeVolume->identification()) { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kTrackGainKey, volumeAdjustment); } else if(TagLib::String("album", TagLib::String::Latin1) == relativeVolume->identification()) { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kAlbumGainKey, volumeAdjustment); } // Fall back to track gain if identification is not specified else { if((int)volumeAdjustment) AddFloatToDictionary(dictionary, Metadata::kTrackGainKey, volumeAdjustment); } } } // Extract album art if present for(auto it : tag->frameListMap()["APIC"]) { TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(it); if(frame) { SFB::CFData data((const UInt8 *)frame->picture().data(), (CFIndex)frame->picture().size()); SFB::CFString description; if(!frame->description().isEmpty()) description = CFString(frame->description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)frame->type(), description)); } } return true; }
Meta::FieldHash ID3v2TagHelper::tags() const { Meta::FieldHash data = TagHelper::tags(); TagLib::ID3v2::FrameList list = m_tag->frameList(); for( TagLib::ID3v2::FrameList::ConstIterator it = list.begin(); it != list.end(); ++it ) { qint64 field; TagLib::String frameName = TagLib::String( ( *it )->frameID() ); if( ( field = fieldName( frameName ) ) ) { if( field == Meta::valUniqueId ) { TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast< TagLib::ID3v2::UniqueFileIdentifierFrame * >( *it ); if( !frame ) continue; QString identifier = TStringToQString( TagLib::String( frame->identifier() ) ); if( identifier.isEmpty() ) continue; if( frame->owner() == uidFieldName( UIDAFT ) && isValidUID( identifier, UIDAFT ) ) data.insert( Meta::valUniqueId, identifier ); continue; } else if( field == Meta::valHasCover ) { TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast< TagLib::ID3v2::AttachedPictureFrame * >( *it ); if( !frame ) continue; if( ( frame->type() == TagLib::ID3v2::AttachedPictureFrame::FrontCover || frame->type() == TagLib::ID3v2::AttachedPictureFrame::Other ) && frame->picture().size() > MIN_COVER_SIZE ) // must be at least 1kb { data.insert( Meta::valHasCover, true ); } continue; } TagLib::ID3v2::TextIdentificationFrame *frame = dynamic_cast< TagLib::ID3v2::TextIdentificationFrame * >( *it ); if( !frame ) continue; QString value = TStringToQString( frame->fieldList().toString( '\n' ) ); if( field == Meta::valDiscNr ) { int disc; if( ( disc = splitDiscNr( value ).first ) ) data.insert( field, disc ); } else if( field == Meta::valBpm ) data.insert( field, value.toFloat() ); else data.insert( field, value ); } else if( frameName == POPM_Frame ) { TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast< TagLib::ID3v2::PopularimeterFrame * >( *it ); if( !frame ) continue; if( TStringToQString( frame->email() ).isEmpty() ) // only read anonymous ratings { // FMPS tags have precedence if( !data.contains( Meta::valRating ) && frame->rating() != 0 ) data.insert( Meta::valRating, qRound( frame->rating() / 256.0 * 10.0 ) ); if( !data.contains( Meta::valPlaycount ) && frame->counter() < 10000 ) data.insert( Meta::valPlaycount, frame->counter() ); } } else if( frameName == TXXX_Frame ) { TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast< TagLib::ID3v2::UserTextIdentificationFrame * >( *it ); if( !frame ) continue; // the value of the user text frame is stored in the // second and following fields. TagLib::StringList fields = frame->fieldList(); if( fields.size() >= 2 ) { QString value = TStringToQString( fields[1] ); if( fields[0] == fmpsFieldName( FMPSRating ) ) data.insert( Meta::valRating, qRound( value.toFloat() * 10.0 ) ); else if( fields[0] == fmpsFieldName( FMPSScore ) ) data.insert( Meta::valScore, value.toFloat() * 100.0 ); else if( fields[0] == fmpsFieldName( FMPSPlayCount ) ) data.insert( Meta::valPlaycount, value.toFloat() ); } } } return data; }