bool SFB::Audio::AddAPETagToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::APE::Tag *tag) { if(nullptr == dictionary || nullptr == tag) return false; if(tag->isEmpty()) return true; SFB::CFMutableDictionary additionalMetadata(0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); for(auto iterator : tag->itemListMap()) { auto item = iterator.second; if(item.isEmpty()) continue; if(TagLib::APE::Item::Text == item.type()) { SFB::CFString key(item.key().toCString(true), kCFStringEncodingUTF8); SFB::CFString value(item.toString().toCString(true), kCFStringEncodingUTF8); if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUM"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSER"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GENRE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGenreKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DATE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kReleaseDateKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DESCRIPTION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCommentKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPILATION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCompilationKey, CFStringGetIntValue(value) ? kCFBooleanTrue : kCFBooleanFalse); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("LYRICS"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kLyricsKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("BPM"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kBPMKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("RATING"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kRatingKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ISRC"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kISRCKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MCN"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMCNKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_ALBUMID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzReleaseIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_TRACKID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzRecordingIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMTITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSERSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GROUPING"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGroupingKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_REFERENCE_LOUDNESS"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, CFStringGetDoubleValue(value)); #if 0 else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("METADATA_BLOCK_PICTURE"), kCFCompareCaseInsensitive)) { // Handle embedded pictures for(auto blockIterator : item.values()) { auto encodedBlock = blockIterator.data(TagLib::String::UTF8); // Decode the Base-64 encoded data auto decodedBlock = TagLib::DecodeBase64(encodedBlock); // Create the picture TagLib::FLAC::Picture picture; picture.parse(decodedBlock); SFB::CFData data((const UInt8 *)picture.data().data(), picture.data().size()); SFB::CFString description = nullptr; if(!picture.description().isNull()) description(picture.description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)picture.type(), description)); } } #endif // Put all unknown tags into the additional metadata else CFDictionarySetValue(additionalMetadata, key, value); } else if(TagLib::APE::Item::Binary == item.type()) { SFB::CFString key(item.key().toCString(true), kCFStringEncodingUTF8); // From http://www.hydrogenaudio.org/forums/index.php?showtopic=40603&view=findpost&p=504669 /* <length> 32 bit <flags with binary bit set> 32 bit <field name> "Cover Art (Front)"|"Cover Art (Back)" 0x00 <description> UTF-8 string (needs to be a file name to be recognized by AudioShell - meh) 0x00 <cover data> binary */ if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Front)"), kCFCompareCaseInsensitive) || kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Back)"), kCFCompareCaseInsensitive)) { auto binaryData = item.binaryData(); size_t pos = binaryData.find('\0'); if(TagLib::ByteVector::npos() != pos && 3 < binaryData.size()) { SFB::CFData data((const UInt8 *)binaryData.mid(pos + 1).data(), (CFIndex)(binaryData.size() - pos - 1)); SFB::CFString description(TagLib::String(binaryData.mid(0, pos), TagLib::String::UTF8).toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, kCFCompareEqualTo == CFStringCompare(key, CFSTR("Cover Art (Front)"), kCFCompareCaseInsensitive) ? AttachedPicture::Type::FrontCover : AttachedPicture::Type::BackCover, description)); } } } } if(CFDictionaryGetCount(additionalMetadata)) CFDictionarySetValue(dictionary, Metadata::kAdditionalMetadataKey, additionalMetadata); return true; }
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; }
bool SFB::Audio::AddXiphCommentToDictionary(CFMutableDictionaryRef dictionary, std::vector<std::shared_ptr<AttachedPicture>>& attachedPictures, const TagLib::Ogg::XiphComment *tag) { if(nullptr == dictionary || nullptr == tag) return false; SFB::CFMutableDictionary additionalMetadata = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); for(auto it : tag->fieldListMap()) { // According to the Xiph comment specification keys should only contain a limited subset of ASCII, but UTF-8 is a safer choice SFB::CFString key = CFStringCreateWithCString(kCFAllocatorDefault, it.first.toCString(true), kCFStringEncodingUTF8); // Vorbis allows multiple comments with the same key, but this isn't supported by AudioMetadata SFB::CFString value = CFStringCreateWithCString(kCFAllocatorDefault, it.second.front().toCString(true), kCFStringEncodingUTF8); if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUM"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTIST"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSER"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GENRE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGenreKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DATE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kReleaseDateKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DESCRIPTION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCommentKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLE"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TRACKTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kTrackTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPILATION"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kCompilationKey, CFStringGetIntValue(value) ? kCFBooleanTrue : kCFBooleanFalse); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCNUMBER"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscNumberKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("DISCTOTAL"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kDiscTotalKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("LYRICS"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kLyricsKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("BPM"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kBPMKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("RATING"), kCFCompareCaseInsensitive)) AddIntToDictionary(dictionary, Metadata::kRatingKey, CFStringGetIntValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ISRC"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kISRCKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MCN"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMCNKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_ALBUMID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzReleaseIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("MUSICBRAINZ_TRACKID"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kMusicBrainzRecordingIDKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("TITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMTITLESORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumTitleSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("ALBUMARTISTSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kAlbumArtistSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("COMPOSERSORT"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kComposerSortOrderKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("GROUPING"), kCFCompareCaseInsensitive)) CFDictionarySetValue(dictionary, Metadata::kGroupingKey, value); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_REFERENCE_LOUDNESS"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kReferenceLoudnessKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_TRACK_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kTrackPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_GAIN"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumGainKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("REPLAYGAIN_ALBUM_PEAK"), kCFCompareCaseInsensitive)) AddDoubleToDictionary(dictionary, Metadata::kAlbumPeakKey, CFStringGetDoubleValue(value)); else if(kCFCompareEqualTo == CFStringCompare(key, CFSTR("METADATA_BLOCK_PICTURE"), kCFCompareCaseInsensitive)) { // Handle embedded pictures for(auto blockIterator : it.second) { auto encodedBlock = blockIterator.data(TagLib::String::UTF8); // Decode the Base-64 encoded data auto decodedBlock = TagLib::DecodeBase64(encodedBlock); // Create the picture TagLib::FLAC::Picture picture; picture.parse(decodedBlock); SFB::CFData data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)picture.data().data(), (CFIndex)picture.data().size()); SFB::CFString description; if(!picture.description().isEmpty()) description = CFStringCreateWithCString(kCFAllocatorDefault, picture.description().toCString(true), kCFStringEncodingUTF8); attachedPictures.push_back(std::make_shared<AttachedPicture>(data, (AttachedPicture::Type)picture.type(), description)); } } // Put all unknown tags into the additional metadata else CFDictionarySetValue(additionalMetadata, key, value); } if(CFDictionaryGetCount(additionalMetadata)) CFDictionarySetValue(dictionary, Metadata::kAdditionalMetadataKey, additionalMetadata); return true; }