/* ** Extracts cover art embedded in FLAC files. ** */ bool CCover::ExtractFLAC(TagLib::FLAC::File* file, const std::wstring& target) { const TagLib::List<TagLib::FLAC::Picture*>& picList = file->pictureList(); if (!picList.isEmpty()) { // Let's grab the first image TagLib::FLAC::Picture* pic = picList[0]; return WriteCover(pic->data(), target); } return false; }
/* ** Extracts cover art embedded in FLAC files. ** */ bool QCoverArt::ExtractFLAC(TagLib::FLAC::File* file) { const TagLib::List<TagLib::FLAC::Picture*>& picList = file->pictureList(); if (!picList.isEmpty()) { // Let's grab the first image TagLib::FLAC::Picture* pic = picList[0]; img.loadFromData((const unsigned char*)pic->data().data(), (int)pic->data().size()); return true; } return false; }
Cover* FileHelper::extractCover() { Cover *cover = nullptr; switch (_fileType) { case EXT_MP3: { TagLib::MPEG::File *mpegFile = static_cast<TagLib::MPEG::File*>(_file); if (mpegFile && mpegFile->hasID3v2Tag()) { // Look for picture frames only TagLib::ID3v2::FrameList listOfMp3Frames = mpegFile->ID3v2Tag()->frameListMap()["APIC"]; // It's possible to have more than one picture per file! if (!listOfMp3Frames.isEmpty()) { for (TagLib::ID3v2::FrameList::ConstIterator it = listOfMp3Frames.begin(); it != listOfMp3Frames.end() ; it++) { // Cast a Frame* to AttachedPictureFrame* TagLib::ID3v2::AttachedPictureFrame *pictureFrame = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(*it); if (pictureFrame) { // Performs a deep copy of the cover QByteArray b = QByteArray(pictureFrame->picture().data(), pictureFrame->picture().size()); cover = new Cover(b, QString(pictureFrame->mimeType().toCString(true))); } } } } else if (mpegFile && mpegFile->hasID3v1Tag()) { qDebug() << Q_FUNC_INFO << "Not implemented for ID3v1Tag"; } break; } case EXT_FLAC: { if (TagLib::FLAC::File *flacFile = static_cast<TagLib::FLAC::File*>(_file)) { auto list = flacFile->pictureList(); for (auto it = list.begin(); it != list.end() ; it++) { TagLib::FLAC::Picture *p = *it; if (p->type() == TagLib::FLAC::Picture::FrontCover) { // Performs a deep copy of the cover QByteArray b = QByteArray(p->data().data(), p->data().size()); cover = new Cover(b, QString(p->mimeType().toCString(true))); break; } } } break; } default: qDebug() << Q_FUNC_INFO << "Not implemented for this file type" << _fileType << _file << _fileInfo.absoluteFilePath(); break; } return cover; }
/** Sets the inner picture. */ void FileHelper::setCover(Cover *cover) { switch (_fileType) { case EXT_MP3: { TagLib::MPEG::File *mpegFile = static_cast<TagLib::MPEG::File*>(_file); if (mpegFile->hasID3v2Tag()) { // Look for picture frames only TagLib::ID3v2::FrameList mp3Frames = mpegFile->ID3v2Tag()->frameListMap()["APIC"]; if (!mp3Frames.isEmpty()) { for (TagLib::ID3v2::FrameList::Iterator it = mp3Frames.begin(); it != mp3Frames.end() ; it++) { // Removing a frame will invalidate any pointers on the list mpegFile->ID3v2Tag()->removeFrame(*it); break; } } if (cover != nullptr) { TagLib::ByteVector bv(cover->byteArray().data(), cover->byteArray().length()); TagLib::ID3v2::AttachedPictureFrame *pictureFrame = new TagLib::ID3v2::AttachedPictureFrame(); pictureFrame->setMimeType(cover->mimeType()); pictureFrame->setPicture(bv); pictureFrame->setType(TagLib::ID3v2::AttachedPictureFrame::FrontCover); mpegFile->ID3v2Tag()->addFrame(pictureFrame); } } else if (mpegFile->hasID3v1Tag()) { qDebug() << Q_FUNC_INFO << "Not implemented for ID3v1Tag"; } break; } case EXT_FLAC: { TagLib::FLAC::File *flacFile = static_cast<TagLib::FLAC::File*>(_file); flacFile->removePictures(); if (cover != nullptr) { TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture; picture->setType(TagLib::FLAC::Picture::FrontCover); TagLib::ByteVector bv(cover->byteArray().data(), cover->byteArray().length()); picture->setData(bv); flacFile->addPicture(picture); } break; } default: qDebug() << Q_FUNC_INFO << "Not implemented for" << _fileType; break; } }
QImage SoundSourceSndFile::parseCoverArt() { QImage coverArt; QString location = getFilename(); setType(location.section(".",-1).toLower()); const QByteArray qBAFilename(getFilename().toLocal8Bit()); if (getType() == "flac") { TagLib::FLAC::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); } if (coverArt.isNull()) { TagLib::Ogg::XiphComment *xiph = f.xiphComment(); if (xiph) { coverArt = Mixxx::getCoverInXiphComment(*xiph); } } if (coverArt.isNull()) { TagLib::List<TagLib::FLAC::Picture*> covers = f.pictureList(); if (!covers.isEmpty()) { std::list<TagLib::FLAC::Picture*>::iterator it = covers.begin(); TagLib::FLAC::Picture* cover = *it; coverArt = QImage::fromData( QByteArray(cover->data().data(), cover->data().size())); } } } else if (getType() == "wav") { TagLib::RIFF::WAV::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); } } else { // Try AIFF TagLib::RIFF::AIFF::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); } } return coverArt; }
QImage SoundSourceFLAC::parseCoverArt() { QImage coverArt; setType("flac"); TagLib::FLAC::File f(m_qFilename.toLocal8Bit().constData()); coverArt = getCoverInID3v2Tag(f.ID3v2Tag()); if (coverArt.isNull()) { coverArt = getCoverInXiphComment(f.xiphComment()); } if (coverArt.isNull()) { TagLib::List<TagLib::FLAC::Picture*> covers = f.pictureList(); if (!covers.isEmpty()) { std::list<TagLib::FLAC::Picture*>::iterator it = covers.begin(); TagLib::FLAC::Picture* cover = *it; coverArt = QImage::fromData( QByteArray(cover->data().data(), cover->data().size())); } } return coverArt; }
bool mediaFLAC(Artwork *art, const char *filePath) { TagLib::FLAC::File f(filePath); if (!f.tag()) { return false; } if (!mediaTag(art, &f)) { return false; } art->filetype = FILETYPE_FLAC; #if 0 const TagLib::List<TagLib::FLAC::Picture*> picturelist = f->pictureList(); for(TagLib::List<TagLib::FLAC::Picture*>::ConstIterator it = picturelist.begin(); it != picturelist.end(); it++) { TagLib::FLAC::Picture *picture = (*it); if (picture) { if (picture->type() == TagLib::FLAC::Picture::FileIcon || picture->type() == TagLib::FLAC::Picture::OtherFileIcon || picture->type( )== TagLib::FLAC::Picture::ColouredFish) { // skip! } else { if (!art->hasCover()) { saveImage(art, (const uchar *) picture->data().data(), picture->data().size()); } break; } } } #endif return art; }
QByteArray TagReader::LoadEmbeddedArt(const QString& filename) const { if (filename.isEmpty()) return QByteArray(); qLog(Debug) << "Loading art from" << filename; #ifdef Q_OS_WIN32 TagLib::FileRef ref(filename.toStdWString().c_str()); #else TagLib::FileRef ref(QFile::encodeName(filename).constData()); #endif if (ref.isNull() || !ref.file()) return QByteArray(); // MP3 TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file()); if (file && file->ID3v2Tag()) { TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"]; if (apic_frames.isEmpty()) return QByteArray(); TagLib::ID3v2::AttachedPictureFrame* pic = static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front()); return QByteArray((const char*)pic->picture().data(), pic->picture().size()); } // Ogg vorbis/speex TagLib::Ogg::XiphComment* xiph_comment = dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag()); if (xiph_comment) { TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap(); // Other than the below mentioned non-standard COVERART, // METADATA_BLOCK_PICTURE // is the proposed tag for cover pictures. // (see http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE) if (map.contains("METADATA_BLOCK_PICTURE")) { TagLib::StringList pict_list = map["METADATA_BLOCK_PICTURE"]; for (std::list<TagLib::String>::iterator it = pict_list.begin(); it != pict_list.end(); ++it) { QByteArray data(QByteArray::fromBase64(it->toCString())); TagLib::ByteVector tdata(data.data(), data.size()); TagLib::FLAC::Picture p(tdata); if (p.type() == TagLib::FLAC::Picture::FrontCover) return QByteArray(p.data().data(), p.data().size()); } // If there was no specific front cover, just take the first picture QByteArray data(QByteArray::fromBase64( map["METADATA_BLOCK_PICTURE"].front().toCString())); TagLib::ByteVector tdata(data.data(), data.size()); TagLib::FLAC::Picture p(tdata); return QByteArray(p.data().data(), p.data().size()); } // Ogg lacks a definitive standard for embedding cover art, but it seems // b64 encoding a field called COVERART is the general convention if (!map.contains("COVERART")) return QByteArray(); return QByteArray::fromBase64(map["COVERART"].toString().toCString()); } #ifdef TAGLIB_HAS_FLAC_PICTURELIST // Flac TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file()); if (flac_file && flac_file->xiphComment()) { TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList(); if (!pics.isEmpty()) { // Use the first picture in the file - this could be made cleverer and // pick the front cover if it's present. std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin(); TagLib::FLAC::Picture* picture = *it; return QByteArray(picture->data().data(), picture->data().size()); } } #endif // MP4/AAC TagLib::MP4::File* aac_file = dynamic_cast<TagLib::MP4::File*>(ref.file()); if (aac_file) { TagLib::MP4::Tag* tag = aac_file->tag(); const TagLib::MP4::ItemListMap& items = tag->itemListMap(); TagLib::MP4::ItemListMap::ConstIterator it = items.find("covr"); if (it != items.end()) { const TagLib::MP4::CoverArtList& art_list = it->second.toCoverArtList(); if (!art_list.isEmpty()) { // Just take the first one for now const TagLib::MP4::CoverArt& art = art_list.front(); return QByteArray(art.data().data(), art.data().size()); } } } return QByteArray(); }
bool CTagLoaderTagLib::ParseTag(Ogg::XiphComment *xiph, EmbeddedArt *art, CMusicInfoTag& tag) { if (!xiph) return false; FLAC::Picture pictures[3]; ReplayGain replayGainInfo; const Ogg::FieldListMap& fieldListMap = xiph->fieldListMap(); for (Ogg::FieldListMap::ConstIterator it = fieldListMap.begin(); it != fieldListMap.end(); ++it) { if (it->first == "ARTIST") SetArtist(tag, StringListToVectorString(it->second)); else if (it->first == "ARTISTS") tag.SetMusicBrainzArtistHints(StringListToVectorString(it->second)); else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST") SetAlbumArtist(tag, StringListToVectorString(it->second)); else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS") tag.SetMusicBrainzAlbumArtistHints(StringListToVectorString(it->second)); else if (it->first == "ALBUM") tag.SetAlbum(it->second.front().to8Bit(true)); else if (it->first == "TITLE") tag.SetTitle(it->second.front().to8Bit(true)); else if (it->first == "TRACKNUMBER") tag.SetTrackNumber(it->second.front().toInt()); else if (it->first == "DISCNUMBER") tag.SetDiscNumber(it->second.front().toInt()); else if (it->first == "YEAR") tag.SetYear(it->second.front().toInt()); else if (it->first == "DATE") tag.SetYear(it->second.front().toInt()); else if (it->first == "GENRE") SetGenre(tag, StringListToVectorString(it->second)); else if (it->first == "COMMENT") tag.SetComment(it->second.front().to8Bit(true)); else if (it->first == "CUESHEET") tag.SetCueSheet(it->second.front().to8Bit(true)); else if (it->first == "ENCODEDBY") {} else if (it->first == "ENSEMBLE") {} else if (it->first == "COMPILATION") tag.SetCompilation(it->second.front().toInt() == 1); else if (it->first == "LYRICS") tag.SetLyrics(it->second.front().to8Bit(true)); else if (it->first == "REPLAYGAIN_TRACK_GAIN") replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toCString(true)); else if (it->first == "REPLAYGAIN_ALBUM_GAIN") replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toCString(true)); else if (it->first == "REPLAYGAIN_TRACK_PEAK") replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toCString(true)); else if (it->first == "REPLAYGAIN_ALBUM_PEAK") replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toCString(true)); else if (it->first == "MUSICBRAINZ_ARTISTID") tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second))); else if (it->first == "MUSICBRAINZ_ALBUMARTISTID") tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second))); else if (it->first == "MUSICBRAINZ_ALBUMARTIST") SetAlbumArtist(tag, StringListToVectorString(it->second)); else if (it->first == "MUSICBRAINZ_ALBUMID") tag.SetMusicBrainzAlbumID(it->second.front().to8Bit(true)); else if (it->first == "MUSICBRAINZ_TRACKID") tag.SetMusicBrainzTrackID(it->second.front().to8Bit(true)); else if (it->first == "RATING") { // Vorbis ratings are a mess because the standard forgot to mention anything about them. // If you want to see how emotive the issue is and the varying standards, check here: // http://forums.winamp.com/showthread.php?t=324512 // The most common standard in that thread seems to be a 0-100 scale for 1-5 stars. // So, that's what we'll support for now. int iRating = it->second.front().toInt(); if (iRating > 0 && iRating <= 100) tag.SetUserrating((iRating / 20) + '0'); } else if (it->first == "METADATA_BLOCK_PICTURE") { const char* b64 = it->second.front().toCString(); std::string decoded_block = Base64::Decode(b64, it->second.front().size()); ByteVector bv(decoded_block.data(), decoded_block.size()); TagLib::FLAC::Picture* pictureFrame = new TagLib::FLAC::Picture(bv); if (pictureFrame->type() == FLAC::Picture::FrontCover) pictures[0].parse(bv); else if (pictureFrame->type() == FLAC::Picture::Other) pictures[1].parse(bv); delete pictureFrame; } else if (it->first == "COVERART") { const char* b64 = it->second.front().toCString(); std::string decoded_block = Base64::Decode(b64, it->second.front().size()); ByteVector bv(decoded_block.data(), decoded_block.size()); pictures[2].setData(bv); // Assume jpeg if (pictures[2].mimeType().isEmpty()) pictures[2].setMimeType("image/jpeg"); } else if (it->first == "COVERARTMIME") { pictures[2].setMimeType(it->second.front()); } else if (g_advancedSettings.m_logLevel == LOG_LEVEL_MAX) CLog::Log(LOGDEBUG, "unrecognized XipComment name: %s", it->first.toCString(true)); } // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = COVERART/COVERARTMIME for (int i = 0; i < 3; ++i) if (pictures[i].data().size()) { std::string mime = pictures[i].mimeType().toCString(); if (mime.compare(0, 6, "image/") != 0) continue; TagLib::uint size = pictures[i].data().size(); tag.SetCoverArtInfo(size, mime); if (art) art->set((const uint8_t*)pictures[i].data().data(), size, mime); break; } if (xiph->comment() != String::null) tag.SetComment(xiph->comment().toCString(true)); tag.SetReplayGain(replayGainInfo); return true; }
bool FLACMetadata::WriteMetadata(CFErrorRef *error) { UInt8 buf [PATH_MAX]; if(!CFURLGetFileSystemRepresentation(mURL, false, buf, PATH_MAX)) return false; auto stream = new TagLib::FileStream(reinterpret_cast<const char *>(buf)); TagLib::FLAC::File file(stream, TagLib::ID3v2::FrameFactory::instance(), false); if(!file.isValid()) { if(error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Not a FLAC file"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } SetXiphCommentFromMetadata(*this, file.xiphComment(), false); // Remove existing cover art file.removePictures(); // Add album art for(auto attachedPicture : GetAttachedPictures()) { CGImageSourceRef imageSource = CGImageSourceCreateWithData(attachedPicture->GetData(), nullptr); if(nullptr == imageSource) { LOGGER_ERR("org.sbooth.AudioEngine.AudioMetadata.FLAC", "Skipping album art (unable to create image)"); continue; } TagLib::FLAC::Picture *picture = new TagLib::FLAC::Picture; picture->setData(TagLib::ByteVector((const char *)CFDataGetBytePtr(attachedPicture->GetData()), (TagLib::uint)CFDataGetLength(attachedPicture->GetData()))); picture->setType(static_cast<TagLib::FLAC::Picture::Type>(attachedPicture->GetType())); if(attachedPicture->GetDescription()) picture->setDescription(TagLib::StringFromCFString(attachedPicture->GetDescription())); // Convert the image's UTI into a MIME type CFStringRef mimeType = UTTypeCopyPreferredTagWithClass(CGImageSourceGetType(imageSource), kUTTagClassMIMEType); if(mimeType) { picture->setMimeType(TagLib::StringFromCFString(mimeType)); CFRelease(mimeType), mimeType = nullptr; } // Flesh out the height, width, and depth CFDictionaryRef imagePropertiesDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nullptr); if(imagePropertiesDictionary) { CFNumberRef imageWidth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelWidth); CFNumberRef imageHeight = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelHeight); CFNumberRef imageDepth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyDepth); int height, width, depth; // Ignore numeric conversion errors CFNumberGetValue(imageWidth, kCFNumberIntType, &width); CFNumberGetValue(imageHeight, kCFNumberIntType, &height); CFNumberGetValue(imageDepth, kCFNumberIntType, &depth); picture->setHeight(height); picture->setWidth(width); picture->setColorDepth(depth); CFRelease(imagePropertiesDictionary), imagePropertiesDictionary = nullptr; } file.addPicture(picture); CFRelease(imageSource), imageSource = nullptr; } if(!file.save()) { if(error) { CFStringRef description = CFCopyLocalizedString(CFSTR("The file “%@” is not a valid FLAC file."), ""); CFStringRef failureReason = CFCopyLocalizedString(CFSTR("Unable to write metadata"), ""); CFStringRef recoverySuggestion = CFCopyLocalizedString(CFSTR("The file's extension may not match the file's type."), ""); *error = CreateErrorForURL(AudioMetadataErrorDomain, AudioMetadataInputOutputError, description, mURL, failureReason, recoverySuggestion); CFRelease(description), description = nullptr; CFRelease(failureReason), failureReason = nullptr; CFRelease(recoverySuggestion), recoverySuggestion = nullptr; } return false; } MergeChangedMetadataIntoMetadata(); return true; }
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::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; }
bool SetAPETagFromMetadata(const AudioMetadata& metadata, TagLib::APE::Tag *tag, bool setAlbumArt) { if(nullptr == tag) return false; // Standard tags SetAPETag(tag, "ALBUM", metadata.GetAlbumTitle()); SetAPETag(tag, "ARTIST", metadata.GetArtist()); SetAPETag(tag, "ALBUMARTIST", metadata.GetAlbumArtist()); SetAPETag(tag, "COMPOSER", metadata.GetComposer()); SetAPETag(tag, "GENRE", metadata.GetGenre()); SetAPETag(tag, "DATE", metadata.GetReleaseDate()); SetAPETag(tag, "DESCRIPTION", metadata.GetComment()); SetAPETag(tag, "TITLE", metadata.GetTitle()); SetAPETagNumber(tag, "TRACKNUMBER", metadata.GetTrackNumber()); SetAPETagNumber(tag, "TRACKTOTAL", metadata.GetTrackTotal()); SetAPETagBoolean(tag, "COMPILATION", metadata.GetCompilation()); SetAPETagNumber(tag, "DISCNUMBER", metadata.GetDiscNumber()); SetAPETagNumber(tag, "DISCTOTAL", metadata.GetDiscTotal()); SetAPETagNumber(tag, "BPM", metadata.GetBPM()); SetAPETagNumber(tag, "RATING", metadata.GetRating()); SetAPETag(tag, "ISRC", metadata.GetISRC()); SetAPETag(tag, "MCN", metadata.GetMCN()); SetAPETag(tag, "TITLESORT", metadata.GetTitleSortOrder()); SetAPETag(tag, "ALBUMTITLESORT", metadata.GetAlbumTitleSortOrder()); SetAPETag(tag, "ARTISTSORT", metadata.GetArtistSortOrder()); SetAPETag(tag, "ALBUMARTISTSORT", metadata.GetAlbumArtistSortOrder()); SetAPETag(tag, "COMPOSERSORT", metadata.GetComposerSortOrder()); SetAPETag(tag, "GROUPING", metadata.GetGrouping()); // Additional metadata CFDictionaryRef additionalMetadata = metadata.GetAdditionalMetadata(); if(nullptr != additionalMetadata) { CFIndex count = CFDictionaryGetCount(additionalMetadata); const void * keys [count]; const void * values [count]; CFDictionaryGetKeysAndValues(additionalMetadata, reinterpret_cast<const void **>(keys), reinterpret_cast<const void **>(values)); for(CFIndex i = 0; i < count; ++i) { CFIndex keySize = CFStringGetMaximumSizeForEncoding(CFStringGetLength(reinterpret_cast<CFStringRef>(keys[i])), kCFStringEncodingASCII); char key [keySize + 1]; if(!CFStringGetCString(reinterpret_cast<CFStringRef>(keys[i]), key, keySize + 1, kCFStringEncodingASCII)) { LOGGER_ERR("org.sbooth.AudioEngine", "CFStringGetCString failed"); continue; } SetAPETag(tag, key, reinterpret_cast<CFStringRef>(values[i])); } } // ReplayGain info SetAPETagDouble(tag, "REPLAYGAIN_REFERENCE_LOUDNESS", metadata.GetReplayGainReferenceLoudness(), CFSTR("%2.1f dB")); SetAPETagDouble(tag, "REPLAYGAIN_TRACK_GAIN", metadata.GetReplayGainTrackGain(), CFSTR("%+2.2f dB")); SetAPETagDouble(tag, "REPLAYGAIN_TRACK_PEAK", metadata.GetReplayGainTrackPeak(), CFSTR("%1.8f")); SetAPETagDouble(tag, "REPLAYGAIN_ALBUM_GAIN", metadata.GetReplayGainAlbumGain(), CFSTR("%+2.2f dB")); SetAPETagDouble(tag, "REPLAYGAIN_ALBUM_PEAK", metadata.GetReplayGainAlbumPeak(), CFSTR("%1.8f")); // Album art if(setAlbumArt) { tag->removeItem("Cover Art (Front)"); tag->removeItem("Cover Art (Back)"); #if 0 tag->removeItem("METADATA_BLOCK_PICTURE"); #endif for(auto attachedPicture : metadata.GetAttachedPictures()) { // APE can handle front and back covers natively if(AttachedPicture::Type::FrontCover == attachedPicture->GetType() || AttachedPicture::Type::FrontCover == attachedPicture->GetType()) { TagLib::ByteVector data; if(attachedPicture->GetDescription()) data.append(TagLib::StringFromCFString(attachedPicture->GetDescription()).data(TagLib::String::UTF8)); data.append('\0'); data.append(TagLib::ByteVector((const char *)CFDataGetBytePtr(attachedPicture->GetData()), (TagLib::uint)CFDataGetLength(attachedPicture->GetData()))); if(AttachedPicture::Type::FrontCover == attachedPicture->GetType()) tag->setData("Cover Art (Front)", data); else if(AttachedPicture::Type::BackCover == attachedPicture->GetType()) tag->setData("Cover Art (Back)", data); } #if 0 else { CGImageSourceRef imageSource = CGImageSourceCreateWithData(attachedPicture->GetData(), nullptr); if(nullptr == imageSource) return false; TagLib::FLAC::Picture picture; picture.setData(TagLib::ByteVector((const char *)CFDataGetBytePtr(attachedPicture->GetData()), (TagLib::uint)CFDataGetLength(attachedPicture->GetData()))); picture.setType(static_cast<TagLib::FLAC::Picture::Type>(attachedPicture->GetType())); if(attachedPicture->GetDescription()) picture.setDescription(TagLib::StringFromCFString(attachedPicture->GetDescription())); // Convert the image's UTI into a MIME type CFStringRef mimeType = UTTypeCopyPreferredTagWithClass(CGImageSourceGetType(imageSource), kUTTagClassMIMEType); if(mimeType) { picture.setMimeType(TagLib::StringFromCFString(mimeType)); CFRelease(mimeType), mimeType = nullptr; } // Flesh out the height, width, and depth CFDictionaryRef imagePropertiesDictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nullptr); if(imagePropertiesDictionary) { CFNumberRef imageWidth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelWidth); CFNumberRef imageHeight = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelHeight); CFNumberRef imageDepth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyDepth); int height, width, depth; // Ignore numeric conversion errors CFNumberGetValue(imageWidth, kCFNumberIntType, &width); CFNumberGetValue(imageHeight, kCFNumberIntType, &height); CFNumberGetValue(imageDepth, kCFNumberIntType, &depth); picture.setHeight(height); picture.setWidth(width); picture.setColorDepth(depth); CFRelease(imagePropertiesDictionary), imagePropertiesDictionary = nullptr; } TagLib::ByteVector encodedBlock = TagLib::EncodeBase64(picture.render()); tag->addValue("METADATA_BLOCK_PICTURE", TagLib::String(encodedBlock, TagLib::String::UTF8), false); CFRelease(imageSource), imageSource = nullptr; } #endif } } return true; }
void import_flac_file(const std::string& filename) { //std::cerr << "import_flac_file " << filename << std::endl; TagLib::FLAC::File file(filename.c_str()); auto tag = file.tag(); auto artist_name = tag->artist().to8Bit(true); auto album_title = tag->album().to8Bit(true); auto track_title = tag->title().to8Bit(true); auto artist = dm::artist::find_by_name(artist_name); if ( artist.is_null() ) { // Create artist. artist.name(artist_name); artist.save(); } auto album = artist.find_album_by_title(album_title); if ( album.is_null() ) { // Create album. album.title(album_title); album.member("artist", json::object{ { "id", artist.id() }, { "name", artist.name() } }); album.save(); // Add album to artist albums. artist.add_album(album); artist.save(); } auto track = album.find_track_by_title_and_number(track_title, tag->track()); // Set/update track attributes. track.title(track_title); track.track_number(tag->track()); track.disc_number(1); // Create track source object. json::object source{ { "name", "local" }, { "uri", filename } }; TagLib::FLAC::Properties* properties = file.audioProperties(); if ( properties ) { track.duration(properties->length()); } TagLib::Ogg::XiphComment* xiph_comment = file.xiphComment(); if ( xiph_comment ) { auto field_map = xiph_comment->fieldListMap(); json::object replaygain; for ( auto& field : field_map ) { if ( field.first == "TRACK NUMBER" || field.first == "TRACKNUMBER" ) { if ( field.second.size() > 0 ) { track.track_number(std::stoi(field.second[0].to8Bit())); } else { std::cerr << "field='" << field.first << "' string list size=" << field.second.size() << std::endl; } } else if ( field.first == "DISC NUMBER" || field.first == "DISCNUMBER" ) { if ( field.second.size() > 0 ) { track.disc_number(std::stoi(field.second[0].to8Bit())); } else { std::cerr << "field='" << field.first << "' string list size=" << field.second.size() << std::endl; } } else if ( field.first == "REPLAYGAIN_REFERENCE_LOUDNESS" ) { auto ref_loudness = std::stod(field.second[0].to8Bit()); replaygain["reference_loudness"] = ref_loudness; } else if ( field.first == "REPLAYGAIN_TRACK_GAIN" ) { auto gain = std::stod(field.second[0].to8Bit()); replaygain["track_gain"] = gain; } } if ( !replaygain.empty() ) { source["replaygain"] = replaygain; } } const TagLib::List<TagLib::FLAC::Picture*>& images = file.pictureList(); if ( images.size() > 0 ) { TagLib::FLAC::Picture* image = images[0]; if ( image->mimeType() == "image/jpeg" ) { auto cover = dm::album_cover::find_by_album_id(album.id()); if ( cover.is_null() ) { cover.format("jpg"); cover.data(reinterpret_cast<const char*>(image->data().data()), image->data().size()); cover.save(); } } else { std::cerr << "unhandled image mime type - " << filename << " images=" << images.size() << ", mime type " << image->mimeType() << std::endl; } } track.artist(artist); track.album(album); track.source(std::move(source)); if ( track.id_is_null() ) { // Create track id. track.save(); // Add new track to album. album.add_track(track); album.save(); } else { track.save(); } }