/* * Copyright (C) 2024 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 "ScanStepAssociateReleaseImages.hpp" #include #include #include #include #include "core/IConfig.hpp" #include "core/IJob.hpp" #include "core/ILogger.hpp" #include "core/Path.hpp" #include "database/Artwork.hpp" #include "database/Db.hpp" #include "database/Directory.hpp" #include "database/Image.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" #include "database/TrackEmbeddedImage.hpp" #include "JobQueue.hpp" #include "ScanContext.hpp" namespace lms::scanner { namespace { struct ReleaseArtworkAssociation { db::ReleaseId releaseId; db::ArtworkId preferredArtworkId; }; using ReleaseArtworkAssociationContainer = std::deque; struct SearchReleaseArtworkParams { const std::vector& releaseImageFileNames; }; db::Artwork::pointer findImageInDirectory(db::Session& session, const SearchReleaseArtworkParams& searchParams, const std::filesystem::path& directoryPath) { db::Artwork::pointer artwork; const db::Directory::pointer directory{ db::Directory::find(session, directoryPath) }; if (directory) // may not exist for releases that are split on different media libraries { for (std::string_view fileStem : searchParams.releaseImageFileNames) { db::Image::FindParameters params; params.setDirectory(directory->getId()); params.setFileStem(fileStem); db::Image::find(session, params, [&](const db::Image::pointer& image) { if (!artwork) artwork = db::Artwork::find(session, image->getId()); }); if (artwork) break; } } return artwork; } db::Artwork::pointer computePreferredReleaseImage(db::Session& session, const SearchReleaseArtworkParams& searchParams, const db::Release::pointer& release) { db::Artwork::pointer artwork; const auto mbid{ release->getMBID() }; if (mbid) { // Find anywhere, since it is suppoed to be unique! db::Image::find(session, db::Image::FindParameters{}.setFileStem(mbid->getAsString()), [&](const db::Image::pointer& image) { if (!artwork) artwork = db::Artwork::find(session, image->getId()); }); } if (!artwork) { std::set releasePaths; db::Directory::FindParameters params; params.setRelease(release->getId()); db::Directory::find(session, params, [&](const db::Directory::pointer& directory) { releasePaths.insert(directory->getAbsolutePath()); }); // Expect layout like this: // Artist/Release/CD1/... // /CD2/... // /cover.jpg if (releasePaths.size() > 1) { const std::filesystem::path releasePath{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; artwork = findImageInDirectory(session, searchParams, releasePath); } if (!artwork) { for (const std::filesystem::path& releasePath : releasePaths) { artwork = findImageInDirectory(session, searchParams, releasePath); if (artwork) break; } } } return artwork; } db::Artwork::pointer computePreferredReleaseArtwork(db::Session& session, const SearchReleaseArtworkParams& searchParams, const db::Release::pointer& release) { db::Artwork::pointer artwork{ computePreferredReleaseImage(session, searchParams, release) }; if (artwork) return artwork; // Fallback on embedded Front image { db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); params.setImageTypes({ db::ImageType::FrontCover }); params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { artwork = db::Artwork::find(session, image->getId()); }); } if (artwork) return artwork; // Fallback on embedded media image { db::TrackEmbeddedImage::FindParameters params; params.setRelease(release->getId()); params.setImageTypes({ db::ImageType::Media }); params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) { artwork = db::Artwork::find(session, image->getId()); }); } return artwork; } void updateReleasePreferredArtwork(db::Session& session, const ReleaseArtworkAssociation& releaseArtworkAssociation) { db::Release::updatePreferredArtwork(session, releaseArtworkAssociation.releaseId, releaseArtworkAssociation.preferredArtworkId); } void updateReleasePreferredArtworks(db::Session& session, ReleaseArtworkAssociationContainer& imageAssociations, bool forceFullBatch) { constexpr std::size_t writeBatchSize{ 50 }; while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty()) { auto transaction{ session.createWriteTransaction() }; for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i) { updateReleasePreferredArtwork(session, imageAssociations.front()); imageAssociations.pop_front(); } } } std::vector constructReleaseImageFileNames() { std::vector res; core::Service::get()->visitStrings("cover-preferred-file-names", [&res](std::string_view fileName) { res.emplace_back(fileName); }, { "cover", "front", "folder", "default" }); return res; } bool fetchNextReleaseIdRange(db::Session& session, db::ReleaseId& lastRetrievedId, db::IdRange& idRange) { constexpr std::size_t readBatchSize{ 100 }; auto transaction{ session.createReadTransaction() }; idRange = db::Release::findNextIdRange(session, lastRetrievedId, readBatchSize); lastRetrievedId = idRange.last; return idRange.isValid(); } class ComputeReleaseArtworkAssociationsJob : public core::IJob { public: ComputeReleaseArtworkAssociationsJob(db::Db& db, const SearchReleaseArtworkParams& searchParams, db::IdRange artistIdRange) : _db{ db } , _searchParams{ searchParams } , _artistIdRange{ artistIdRange } { } std::span getAssociations() const { return _associations; } std::size_t getProcessedReleaseCount() const { return _processedReleaseCount; } private: core::LiteralString getName() const override { return "Associate Release Artworks"; } void run() override { auto& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; db::Release::find(session, _artistIdRange, [this, &session](const db::Release::pointer& release) { const db::Artwork::pointer preferredArtwork{ computePreferredReleaseArtwork(session, _searchParams, release) }; if (release->getPreferredArtwork() != preferredArtwork) { _associations.push_back(ReleaseArtworkAssociation{ release->getId(), preferredArtwork ? preferredArtwork->getId() : db::ArtworkId{} }); if (preferredArtwork) LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork for release '" << release->getName() << "' with image in " << preferredArtwork->getAbsoluteFilePath()); else LMS_LOG(DBUPDATER, DEBUG, "Removing preferred artwork from release '" << release->getName() << "'"); } _processedReleaseCount++; }); } db::Db& _db; const SearchReleaseArtworkParams& _searchParams; db::IdRange _artistIdRange; std::vector _associations; std::size_t _processedReleaseCount{}; }; } // namespace ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams) : ScanStepBase{ initParams } , _releaseImageFileNames{ constructReleaseImageFileNames() } { } bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const { return context.stats.nbChanges() > 0; } void ScanStepAssociateReleaseImages::process(ScanContext& context) { auto& session{ _db.getTLSSession() }; { auto transaction{ session.createReadTransaction() }; context.currentStepStats.totalElems = db::Release::getCount(session); } const SearchReleaseArtworkParams searchParams{ .releaseImageFileNames = _releaseImageFileNames, }; ReleaseArtworkAssociationContainer artistArtworkAssociations; auto processJobsDone = [&](std::span> jobs) { if (_abortScan) return; for (const auto& job : jobs) { const auto& associationJob{ static_cast(*job) }; const auto& artistAssociations{ associationJob.getAssociations() }; artistArtworkAssociations.insert(std::end(artistArtworkAssociations), std::cbegin(artistAssociations), std::cend(artistAssociations)); context.currentStepStats.processedElems += associationJob.getProcessedReleaseCount(); } updateReleasePreferredArtworks(session, artistArtworkAssociations, true); _progressCallback(context.currentStepStats); }; JobQueue queue{ getJobScheduler(), 20, processJobsDone, 1, 0.85F }; db::ReleaseId lastRetrievedReleaseId{}; db::IdRange artistIdRange; while (fetchNextReleaseIdRange(session, lastRetrievedReleaseId, artistIdRange)) queue.push(std::make_unique(_db, searchParams, artistIdRange)); queue.finish(); // process all remaining associations updateReleasePreferredArtworks(session, artistArtworkAssociations, false); } } // namespace lms::scanner