/* * Copyright (C) 2013 Emeric Poupon * * This file is part of LMS. * * LMS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ #include "CoverService.hpp" #include #include "av/IAudioFile.hpp" #include "database/Db.hpp" #include "database/Artist.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "image/Exception.hpp" #include "image/Image.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/Path.hpp" #include "core/Random.hpp" #include "core/String.hpp" #include "core/Utils.hpp" namespace lms::cover { namespace { struct TrackInfo { bool hasCover{}; bool isMultiDisc{}; std::filesystem::path trackPath; std::optional releaseId; }; std::optional getTrackInfo(db::Session& dbSession, db::TrackId trackId) { std::optional res; auto transaction{ dbSession.createReadTransaction() }; const db::Track::pointer track{ db::Track::find(dbSession, trackId) }; if (!track) return res; res = TrackInfo{}; res->hasCover = track->hasCover(); res->trackPath = track->getPath(); if (const db::Release::pointer & release{ track->getRelease() }) { res->releaseId = release->getId(); if (release->getTotalDisc() > 1) res->isMultiDisc = true; } return res; } std::vector constructPreferredFileNames() { std::vector res; core::Service::get()->visitStrings("cover-preferred-file-names", [&res](std::string_view fileName) { res.emplace_back(fileName); }, { "cover", "front" }); return res; } std::vector constructArtistFileNames() { std::vector res; core::Service::get()->visitStrings("artist-image-file-names", [&res](std::string_view fileName) { res.emplace_back(fileName); }, { "artist" }); return res; } bool isFileSupported(const std::filesystem::path& file, const std::vector& extensions) { return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions)); } } std::unique_ptr createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath) { return std::make_unique(db, defaultSvgCoverPath); } using namespace image; CoverService::CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath) : _db{ db } , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } , _maxFileSize{ core::Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _preferredFileNames{ constructPreferredFileNames() } , _artistFileNames{ constructArtistFileNames() } { setJpegQuality(core::Service::get()->getULong("cover-jpeg-quality", 75)); LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'"); LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize()); LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize); LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ",")); _defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw } std::unique_ptr CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const { std::unique_ptr image; input.visitAttachedPictures([&](const av::Picture& picture) { if (image) return; try { std::unique_ptr rawImage{ decodeImage(picture.data, picture.dataSize) }; rawImage->resize(width); image = rawImage->encodeToJPEG(_jpegQuality); } catch (const image::Exception& e) { LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what()); } }); return image; } std::unique_ptr CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const { std::unique_ptr image; try { std::unique_ptr rawImage{ decodeImage(p) }; rawImage->resize(width); image = rawImage->encodeToJPEG(_jpegQuality); } catch (const image::Exception& e) { LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what()); } return image; } std::shared_ptr CoverService::getDefaultSvgCover() { return _defaultCover; } std::unique_ptr CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector& preferredFileNames, bool allowPickRandom) const { const std::multimap coverPaths{ getCoverPaths(directory) }; auto tryLoadImageFromFilename = [&](std::string_view fileName) { std::unique_ptr image; auto range{ coverPaths.equal_range(std::string {fileName}) }; for (auto it{ range.first }; it != range.second; ++it) { image = getFromCoverFile(it->second, width); if (image) break; } return image; }; std::unique_ptr image; for (const std::string_view filename : preferredFileNames) { image = tryLoadImageFromFilename(filename); if (image) return image; } if (allowPickRandom) { for (const auto& [filename, coverPath] : coverPaths) { image = getFromCoverFile(coverPath, width); if (image) return image; } } return image; } std::unique_ptr CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const { std::unique_ptr res; std::filesystem::path coverPath{ filePath }; for (const std::filesystem::path& extension : _fileExtensions) { coverPath.replace_extension(extension); if (!checkCoverFile(coverPath)) continue; res = getFromCoverFile(coverPath, width); if (res) break; } return res; } bool CoverService::checkCoverFile(const std::filesystem::path& filePath) const { std::error_code ec; if (!isFileSupported(filePath, _fileExtensions)) return false; if (!std::filesystem::exists(filePath, ec)) return false; if (!std::filesystem::is_regular_file(filePath, ec)) return false; if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec) { LMS_LOG(COVER, INFO, "Image file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize); return false; } return true; } std::multimap CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const { std::multimap res; std::error_code ec; std::filesystem::directory_iterator itPath(directoryPath, ec); const std::filesystem::directory_iterator itEnd; while (!ec && itPath != itEnd) { const std::filesystem::path& path{ *itPath }; if (checkCoverFile(path)) res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path); itPath.increment(ec); } return res; } std::unique_ptr CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const { std::unique_ptr image; try { image = getFromAvMediaFile(*av::parseAudioFile(p), width); } catch (av::Exception& e) { LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what()); } return image; } std::shared_ptr CoverService::getFromTrack(db::TrackId trackId, ImageSize width) { return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); } std::shared_ptr CoverService::getFromTrack(db::Session& dbSession, db::TrackId trackId, ImageSize width, bool allowReleaseFallback) { using namespace db; const ImageCache::EntryDesc cacheEntryDesc{ trackId, width }; std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; if (cover) return cover; if (const std::optional trackInfo{ getTrackInfo(dbSession, trackId) }) { if (trackInfo->hasCover) cover = getFromTrack(trackInfo->trackPath, width); if (!cover) cover = getFromSameNamedFile(trackInfo->trackPath, width); if (!cover && trackInfo->releaseId && allowReleaseFallback) cover = getFromRelease(*trackInfo->releaseId, width); if (!cover && trackInfo->isMultiDisc) { if (trackInfo->trackPath.parent_path().has_parent_path()) cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width, _preferredFileNames, true); } } if (cover) _cache.addImage(cacheEntryDesc, cover); return cover; } std::shared_ptr CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width) { using namespace db; const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width }; std::shared_ptr cover{ _cache.getImage(cacheEntryDesc) }; if (cover) return cover; struct ReleaseInfo { TrackId firstTrackId; std::filesystem::path releaseDirectory; }; Session& session{ _db.getTLSSession() }; auto getReleaseInfo{ [&] { std::optional res; auto transaction{ session.createReadTransaction() }; // get a track in this release, consider the release is in a single directory const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) }; if (!tracks.results.empty()) { const Track::pointer& track{ tracks.results.front() }; res = ReleaseInfo{}; res->firstTrackId = track->getId(); res->releaseDirectory = track->getPath().parent_path(); } return res; } }; if (const std::optional releaseInfo{ getReleaseInfo() }) { cover = getFromDirectory(releaseInfo->releaseDirectory, width, _preferredFileNames, true); if (!cover) cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); } if (cover) _cache.addImage(cacheEntryDesc, cover); return cover; } std::shared_ptr CoverService::getFromArtist(db::ArtistId artistId, ImageSize width) { using namespace db; const ImageCache::EntryDesc cacheEntryDesc{ artistId, width }; std::shared_ptr artistImage{ _cache.getImage(cacheEntryDesc) }; if (artistImage) return artistImage; std::string artistName; std::string artistMBID; std::set releasePaths; std::set multiArtistReleasePaths; { Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; const Artist::pointer artist{ Artist::find(session, artistId) }; if (!artist) return artistImage; artistName = artist->getName(); if (auto mbid{ artist->getMBID() }) artistMBID = mbid->getAsString(); Track::FindParameters params; params.setArtist(artistId, { TrackArtistLinkType::ReleaseArtist }); Track::find(session, params, [&](const Track::pointer& track) { Artist::FindParameters artistFindParams; artistFindParams.setTrack(track->getId()); artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist); const auto releaseArtists{ Artist::findIds(session, artistFindParams) }; if (releaseArtists.results.size() == 1) releasePaths.insert(track->getPath().parent_path()); else multiArtistReleasePaths.insert(track->getPath().parent_path()); }); } std::vector artistFileNames; if (!artistMBID.empty()) artistFileNames.push_back(artistMBID); artistFileNames.push_back(artistName); std::vector artistFileNamesWithGenericNames{ artistFileNames }; artistFileNamesWithGenericNames.insert(artistFileNamesWithGenericNames.end(), std::cbegin(_artistFileNames), std::cend(_artistFileNames)); // Expect layout like this: // ReleaseArtist/Release/Tracks' // /artist-mbid.jpg // /artist-name.jpg // /artist.jpg if (!releasePaths.empty()) { const std::filesystem::path artistPath{ releasePaths.size() == 1 ? releasePaths.begin()->parent_path() : core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; artistImage = getFromDirectory(artistPath, width, artistFileNamesWithGenericNames, false); } // Expect layout like this: // ReleaseArtist/Release/Tracks' // /artist-mbid.jpg // /artist-name.jpg // /artist.jpg if (!artistImage) { for (const std::filesystem::path& releasePath : releasePaths) { artistImage = getFromDirectory(releasePath, width, artistFileNamesWithGenericNames, false); if (artistImage) break; } } // Expect layout like this: // Only search for the artist's name in the release path, as we can't map a generic name to several artists // ReleaseArtist/Release/Tracks' // /artist-name.jpg // /artist-mbid.jpg if (!artistImage) { for (const std::filesystem::path& releasePath : multiArtistReleasePaths) { artistImage = getFromDirectory(releasePath, width, artistFileNames, false); if (artistImage) break; } } if (artistImage) _cache.addImage(cacheEntryDesc, artistImage); return artistImage; } void CoverService::flushCache() { } void CoverService::setJpegQuality(unsigned quality) { _jpegQuality = core::utils::clamp(quality, 1, 100); LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality); } } // namespace lms::cover