diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 86526dab..0912828e 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -34,445 +34,425 @@ #include "utils/String.hpp" #include "utils/Utils.hpp" -namespace +namespace Cover { - struct TrackInfo - { - bool hasCover {}; - bool isMultiDisc {}; - std::filesystem::path trackPath; - std::optional releaseId; - }; - std::optional - getTrackInfo(Database::Session& dbSession, Database::TrackId trackId) - { - std::optional res; + namespace + { + struct TrackInfo + { + bool hasCover{}; + bool isMultiDisc{}; + std::filesystem::path trackPath; + std::optional releaseId; + }; - auto transaction {dbSession.createSharedTransaction()}; + std::optional getTrackInfo(Database::Session& dbSession, Database::TrackId trackId) + { + std::optional res; - const Database::Track::pointer track {Database::Track::find(dbSession, trackId)}; - if (!track) - return res; + auto transaction{ dbSession.createSharedTransaction() }; - res = TrackInfo {}; + const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) }; + if (!track) + return res; - res->hasCover = track->hasCover(); - res->trackPath = track->getPath(); + res = TrackInfo{}; - if (const Database::Release::pointer& release {track->getRelease()}) - { - res->releaseId = release->getId(); - if (release->getTotalDisc() > 1) - res->isMultiDisc = true; - } + res->hasCover = track->hasCover(); + res->trackPath = track->getPath(); - return res; - } + if (const Database::Release::pointer & release{ track->getRelease() }) + { + res->releaseId = release->getId(); + if (release->getTotalDisc() > 1) + res->isMultiDisc = true; + } - std::vector constructPreferredFileNames() - { - std::vector res; + return res; + } - Service::get()->visitStrings("cover-preferred-file-names", - [&res](std::string_view fileName) - { - res.emplace_back(fileName); - }, {"cover", "front"}); + std::vector constructPreferredFileNames() + { + std::vector res; - return res; - } -} + Service::get()->visitStrings("cover-preferred-file-names", + [&res](std::string_view fileName) + { + res.emplace_back(fileName); + }, { "cover", "front" }); -namespace Cover { + return res; + } -using namespace Image; + 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)); + } + } -static -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(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) + { + return std::make_unique(db, execPath, defaultCoverPath); + } -std::unique_ptr -createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) -{ - return std::make_unique(db, execPath, defaultCoverPath); -} + using namespace Image; -CoverService::CoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath) - : _db {db} - , _defaultCoverPath {defaultCoverPath} - , _maxCacheSize {Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000} - , _maxFileSize {Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000} - , _preferredFileNames {constructPreferredFileNames()} + CoverService::CoverService(Database::Db& db, + const std::filesystem::path& execPath, + const std::filesystem::path& defaultCoverPath) + : _db{ db } + , _defaultCoverPath{ defaultCoverPath } + , _maxCacheSize{ Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } + , _maxFileSize{ Service::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } + , _preferredFileNames{ constructPreferredFileNames() } -{ - setJpegQuality(Service::get()->getULong("cover-jpeg-quality", 75)); + { + setJpegQuality(Service::get()->getULong("cover-jpeg-quality", 75)); - LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; - LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; - LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; - LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","); + LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'"; + LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize; + LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize; + LMS_LOG(COVER, INFO) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","); #if LMS_SUPPORT_IMAGE_GM - GraphicsMagick::init(execPath); + GraphicsMagick::init(execPath); #else - (void)execPath; + (void)execPath; #endif - try - { - getDefault(512); - } - catch (const Image::ImageException& e) - { - throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); - } -} - -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::ImageException& 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 ImageException& e) - { - LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what(); - } - - return image; -} - -std::shared_ptr -CoverService::getDefault(ImageSize width) -{ - { - std::shared_lock lock {_cacheMutex}; - - if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) - return it->second; - } - - { - std::unique_lock lock {_cacheMutex}; - - if (auto it {_defaultCoverCache.find(width)}; it != std::cend(_defaultCoverCache)) - return it->second; - - std::shared_ptr image {getFromCoverFile(_defaultCoverPath, width)}; - _defaultCoverCache[width] = image; - LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); - - return image; - } -} - -std::unique_ptr -CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) 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 (std::string_view filename : _preferredFileNames) - { - image = tryLoadImageFromFilename(filename); - if (image) - return image; - } - - // Just pick one - 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) << "Cover 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); - 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(Database::TrackId trackId, ImageSize width) -{ - return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); -} - -std::shared_ptr -CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) -{ - using namespace Database; - - const CacheEntryDesc cacheEntryDesc {trackId, width}; - - std::shared_ptr cover {loadFromCache(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); - } - } - - if (!cover) - cover = getDefault(width); - - if (cover) - saveToCache(cacheEntryDesc, cover); - - return cover; -} - -std::shared_ptr -CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) -{ - using namespace Database; - const CacheEntryDesc cacheEntryDesc {releaseId, width}; - - std::shared_ptr cover {loadFromCache(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.createSharedTransaction()}; - - const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release))}; - - if (!tracks.results.empty()) - { - if (const Track::pointer track {Track::find(session, 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); - if (!cover) - cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); - } - - if (!cover) - cover = getDefault(width); - - if (cover) - saveToCache(cacheEntryDesc, cover); - - return cover; -} - -void -CoverService::flushCache() -{ - std::unique_lock lock {_cacheMutex}; - - LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize; - _cacheHits = 0; - _cacheMisses = 0; - _cacheSize = 0; - _cache.clear(); -} - -void -CoverService::setJpegQuality(unsigned quality) -{ - _jpegQuality = Utils::clamp(quality, 1, 100); - - LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; -} - -void -CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) -{ - std::unique_lock lock {_cacheMutex}; - - while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) - { - auto itRandom {Random::pickRandom(_cache)}; - _cacheSize -= itRandom->second->getDataSize(); - _cache.erase(itRandom); - } - - _cacheSize += image->getDataSize(); - _cache[entryDesc] = image; -} - -std::shared_ptr -CoverService::loadFromCache(const CacheEntryDesc& entryDesc) -{ - std::shared_lock lock {_cacheMutex}; - - auto it {_cache.find(entryDesc)}; - if (it == std::cend(_cache)) - { - ++_cacheMisses; - return nullptr; - } - - ++_cacheHits; - return it->second; -} + try + { + getDefault(512); + } + catch (const Image::ImageException& e) + { + throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what()); + } + } + + 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::ImageException& 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 ImageException& e) + { + LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what(); + } + + return image; + } + + std::shared_ptr CoverService::getDefault(ImageSize width) + { + { + std::shared_lock lock{ _cacheMutex }; + + if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) + return it->second; + } + + { + std::unique_lock lock{ _cacheMutex }; + + if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache)) + return it->second; + + std::shared_ptr image{ getFromCoverFile(_defaultCoverPath, width) }; + _defaultCoverCache[width] = image; + LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size(); + + return image; + } + } + + std::unique_ptr CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) 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 (std::string_view filename : _preferredFileNames) + { + image = tryLoadImageFromFilename(filename); + if (image) + return image; + } + + // Just pick one + 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) << "Cover 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); + 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(Database::TrackId trackId, ImageSize width) + { + return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/); + } + + std::shared_ptr CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback) + { + using namespace Database; + + const CacheEntryDesc cacheEntryDesc{ trackId, width }; + + std::shared_ptr cover{ loadFromCache(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); + } + } + + if (!cover) + cover = getDefault(width); + + if (cover) + saveToCache(cacheEntryDesc, cover); + + return cover; + } + + std::shared_ptr CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) + { + using namespace Database; + const CacheEntryDesc cacheEntryDesc{ releaseId, width }; + + std::shared_ptr cover{ loadFromCache(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.createSharedTransaction() }; + + const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({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); + if (!cover) + cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); + } + + if (!cover) + cover = getDefault(width); + + if (cover) + saveToCache(cacheEntryDesc, cover); + + return cover; + } + + void CoverService::flushCache() + { + std::unique_lock lock{ _cacheMutex }; + + LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize; + _cacheHits = 0; + _cacheMisses = 0; + _cacheSize = 0; + _cache.clear(); + } + + void CoverService::setJpegQuality(unsigned quality) + { + _jpegQuality = Utils::clamp(quality, 1, 100); + + LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality; + } + + void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image) + { + std::unique_lock lock{ _cacheMutex }; + + while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty()) + { + auto itRandom{ Random::pickRandom(_cache) }; + _cacheSize -= itRandom->second->getDataSize(); + _cache.erase(itRandom); + } + + _cacheSize += image->getDataSize(); + _cache[entryDesc] = image; + } + + std::shared_ptr CoverService::loadFromCache(const CacheEntryDesc& entryDesc) + { + std::shared_lock lock{ _cacheMutex }; + + auto it{ _cache.find(entryDesc) }; + if (it == std::cend(_cache)) + { + ++_cacheMisses; + return nullptr; + } + + ++_cacheHits; + return it->second; + } } // namespace Cover diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index 7c9683e0..be1f0a16 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -39,11 +39,12 @@ namespace Database { namespace { - Wt::Dbo::Query createQuery(Session& session, const Track::FindParameters& params) + template + Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Track::FindParameters& params) { session.checkSharedLocked(); - auto query{ session.getDboSession().query(params.distinct ? "SELECT DISTINCT t.id FROM track t" : "SELECT t.id FROM track t") }; + auto query{ session.getDboSession().query(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") }; assert(params.keywords.empty() || params.name.empty()); for (std::string_view keyword : params.keywords) @@ -168,6 +169,21 @@ namespace Database return query; } + + template + Wt::Dbo::Query createQuery(Session& session, const Track::FindParameters& params) + { + std::string_view itemToSelect; + + if constexpr (std::is_same_v) + itemToSelect = "t.id"; + else if constexpr (std::is_same_v>) + itemToSelect = "t"; + else + static_assert("Unhandled type"); + + return createQuery(session, itemToSelect, params); + } } Track::Track(const std::filesystem::path& p) @@ -257,7 +273,7 @@ namespace Database return res; } - RangeResults Track::findTrackMBIDDuplicates(Session& session, Range range) + RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, Range range) { session.checkSharedLocked(); @@ -267,7 +283,7 @@ namespace Database return Utils::execQuery(query, range); } - RangeResults Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range) + RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range) { session.checkSharedLocked(); @@ -295,16 +311,23 @@ namespace Database return std::vector(res.begin(), res.end()); } - RangeResults Track::find(Session& session, const FindParameters& parameters) + RangeResults Track::findIds(Session& session, const FindParameters& parameters) { session.checkSharedLocked(); - auto query{ createQuery(session, parameters) }; - + auto query{ createQuery(session, parameters) }; return Utils::execQuery(query, parameters.range); } - RangeResults Track::findSimilarTracks(Session& session, const std::vector& tracks, Range range) + RangeResults Track::find(Session& session, const FindParameters& parameters) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, parameters) }; + return Utils::execQuery(query, parameters.range); + } + + RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, Range range) { assert(!tracks.empty()); session.checkSharedLocked(); diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index d95eee0a..e15aaf1e 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -21,11 +21,10 @@ #include #include -#include +#include #include #include #include -#include #include #include @@ -111,12 +110,13 @@ class Track final : public Object static bool exists(Session& session, TrackId id); static std::vector findByRecordingMBID(Session& session, const UUID& MBID); static std::vector findByMBID(Session& session, const UUID& MBID); - static RangeResults findSimilarTracks(Session& session, const std::vector& trackIds, Range range); + static RangeResults findSimilarTrackIds(Session& session, const std::vector& trackIds, Range range); - static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findIds(Session& session, const FindParameters& parameters); + static RangeResults find(Session& session, const FindParameters& parameters); static RangeResults findPaths(Session& session, Range range); - static RangeResults findTrackMBIDDuplicates(Session& session, Range range); - static RangeResults findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); + static RangeResults findIdsTrackMBIDDuplicates(Session& session, Range range); + static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range); // Accessors void setScanVersion(std::size_t version) { _scanVersion = version; } diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index fe849cf9..607235b3 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -23,415 +23,414 @@ using namespace Database; TEST_F(DatabaseFixture, Artist) { - { - auto transaction {session.createSharedTransaction()}; - EXPECT_FALSE(Artist::exists(session, 35)); - EXPECT_FALSE(Artist::exists(session, 0)); - EXPECT_FALSE(Artist::exists(session, 1)); - EXPECT_EQ(Artist::getCount(session), 0); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_FALSE(Artist::exists(session, 35)); + EXPECT_FALSE(Artist::exists(session, 0)); + EXPECT_FALSE(Artist::exists(session, 1)); + EXPECT_EQ(Artist::getCount(session), 0); + } - ScopedArtist artist {session, "MyArtist"}; + ScopedArtist artist{ session, "MyArtist" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(artist.get()); - EXPECT_FALSE(!artist.get()); - EXPECT_EQ(artist.get()->getId(), artist.getId()); + EXPECT_TRUE(artist.get()); + EXPECT_FALSE(!artist.get()); + EXPECT_EQ(artist.get()->getId(), artist.getId()); - EXPECT_TRUE(Artist::exists(session, artist.getId())); - EXPECT_EQ(Artist::getCount(session), 1); - } + EXPECT_TRUE(Artist::exists(session, artist.getId())); + EXPECT_EQ(Artist::getCount(session), 1); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {Artist::find(session, Artist::FindParameters {})}; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + auto artists{ Artist::find(session, Artist::FindParameters {}) }; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::findAllOrphans(session, Range {}); - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); - } + artists = Artist::findAllOrphans(session, Range{}); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); + } } TEST_F(DatabaseFixture, Artist_singleTrack) { - ScopedTrack track {session, "MyTrack"}; - ScopedArtist artist {session, "MyArtist"}; + ScopedTrack track{ session, "MyTrack" }; + ScopedArtist artist{ session, "MyArtist" }; - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - track.get().modify()->setName("MyTrackName"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - } + track.get().modify()->setName("MyTrackName"); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {track->getArtists({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists{ track->getArtists({TrackArtistLinkType::Artist}) }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist.getId()); - ASSERT_EQ(track->getArtistLinks().size(), 1); - auto artistLink {track->getArtistLinks().front()}; - EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); - EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); + ASSERT_EQ(track->getArtistLinks().size(), 1); + auto artistLink{ track->getArtistLinks().front() }; + EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); + EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); - ASSERT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 1); - EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); - EXPECT_EQ(track->getArtists({}).size(), 1); - } + ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1); + EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty()); + EXPECT_EQ(track->getArtists({}).size(), 1); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {track->getArtistIds({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front(), artist.getId()); + auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); - ASSERT_EQ(track->getArtistIds({TrackArtistLinkType::Artist}).size(), 1); - EXPECT_TRUE(track->getArtistIds({TrackArtistLinkType::ReleaseArtist}).empty()); - EXPECT_EQ(track->getArtistIds({}).size(), 1); - } + ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1); + EXPECT_TRUE(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).empty()); + EXPECT_EQ(track->getArtistIds({}).size(), 1); + } - { - auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))}; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); - } - { - auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))}; - EXPECT_EQ(tracks.results.size(), 0); - } - { - auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))}; - EXPECT_EQ(tracks.results.size(), 0); - } - { - auto transaction {session.createSharedTransaction()}; - auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))}; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) }; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); + } + { + auto transaction{ session.createSharedTransaction() }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) }; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction{ session.createSharedTransaction() }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) }; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction{ session.createSharedTransaction() }; + auto artists{ Artist::find(session, Artist::FindParameters{}.setTrack(track->getId())) }; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); + } } TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) { - ScopedTrack track {session, "MyTrack"}; - ScopedArtist artist {session, "MyArtist"}; - { - auto transaction {session.createUniqueTransaction()}; + ScopedTrack track{ session, "MyTrack" }; + ScopedArtist artist{ session, "MyArtist" }; + { + auto transaction{ session.createUniqueTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); - } + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); + } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); + } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {track->getArtists({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists{ track->getArtists({TrackArtistLinkType::Artist}) }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist.getId()); - artists = track->getArtists({TrackArtistLinkType::ReleaseArtist}); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist }); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist.getId()); - EXPECT_EQ(track->getArtistLinks().size(), 3); + EXPECT_EQ(track->getArtistLinks().size(), 3); - auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()))}; - EXPECT_EQ(tracks.results.size(), 1); + auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId())) }; + EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist})); - EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})); - EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Writer})); - EXPECT_EQ(tracks.results.size(), 1); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })); + EXPECT_EQ(tracks.results.size(), 1); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })); + EXPECT_EQ(tracks.results.size(), 1); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer })); + EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer})); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer })); + EXPECT_EQ(tracks.results.size(), 0); + } - { - auto transaction {session.createSharedTransaction()}; - EnumSet types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; - EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); - EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); - EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); - EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); - } + { + auto transaction{ session.createSharedTransaction() }; + EnumSet types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; + EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); + EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); + EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); + EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); + } } TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { - ScopedTrack track {session, "track"}; - ScopedArtist artist1 {session, "artist1"}; - ScopedArtist artist2 {session, "artist2"}; - ASSERT_NE(artist1.getId(), artist2.getId()); + ScopedTrack track{ session, "track" }; + ScopedArtist artist1{ session, "artist1" }; + ScopedArtist artist2{ session, "artist2" }; + ASSERT_NE(artist1.getId(), artist2.getId()); - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); - } + TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); + } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_TRUE(Artist::findAllOrphans(session, Range{}).results.empty()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {track->getArtists({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 2); - EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId()) - || (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId())); + auto artists{ track->getArtists({TrackArtistLinkType::Artist}) }; + ASSERT_EQ(artists.size(), 2); + EXPECT_TRUE((artists[0]->getId() == artist1.getId() && artists[1]->getId() == artist2.getId()) + || (artists[0]->getId() == artist2.getId() && artists[1]->getId() == artist1.getId())); - EXPECT_EQ(track->getArtists({}).size(), 2); - EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); - EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2); - EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); - } + EXPECT_EQ(track->getArtists({}).size(), 2); + EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2); + EXPECT_TRUE(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).empty()); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}).results.size(), 2); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))}; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track->getId()); + auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) }; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track->getId()); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId())); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track->getId()); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId())); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track->getId()); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::ReleaseArtist})); - EXPECT_EQ(tracks.results.size(), 0); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist })); + EXPECT_EQ(tracks.results.size(), 0); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist1->getId(), {TrackArtistLinkType::Artist})); - EXPECT_EQ(tracks.results.size(), 1); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist })); + EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::ReleaseArtist})); - EXPECT_EQ(tracks.results.size(), 0); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist })); + EXPECT_EQ(tracks.results.size(), 0); - tracks = Track::find(session, Track::FindParameters {}.setArtist(artist2->getId(), {TrackArtistLinkType::Artist})); - EXPECT_EQ(tracks.results.size(), 1); - } + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); + EXPECT_EQ(tracks.results.size(), 1); + } } TEST_F(DatabaseFixture, Artist_findByName) { - ScopedArtist artist {session, "AAA"}; - ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans + ScopedArtist artist{ session, "AAA" }; + ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans - { - auto transaction {session.createUniqueTransaction()}; - artist.get().modify()->setSortName("ZZZ"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - } + { + auto transaction{ session.createUniqueTransaction() }; + artist.get().modify()->setSortName("ZZZ"); + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty()); - const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))}; - ASSERT_EQ(artistsByAAA.results.size(), 1); - EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); + const auto artistsByAAA{ Artist::find(session, Artist::FindParameters {}.setKeywords({"A"})) }; + ASSERT_EQ(artistsByAAA.results.size(), 1); + EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); - const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))}; - ASSERT_EQ(artistsByZZZ.results.size(), 1); - EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); + const auto artistsByZZZ{ Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"})) }; + ASSERT_EQ(artistsByZZZ.results.size(), 1); + EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); - EXPECT_TRUE(Artist::find(session, "NNN").empty()); - EXPECT_EQ(Artist::find(session, "AAA").size(), 1); - } + EXPECT_TRUE(Artist::find(session, "NNN").empty()); + EXPECT_EQ(Artist::find(session, "AAA").size(), 1); + } } TEST_F(DatabaseFixture, Artist_findByNameEscaped) { - ScopedArtist artist1 {session, "MyArtist%"}; - ScopedArtist artist2 {session, "%MyArtist"}; - ScopedArtist artist3 {session, "%_MyArtist"}; + ScopedArtist artist1{ session, "MyArtist%" }; + ScopedArtist artist2{ session, "%MyArtist" }; + ScopedArtist artist3{ session, "%_MyArtist" }; - ScopedArtist artist4 {session, "MyArtist%foo"}; - ScopedArtist artist5 {session, "foo%MyArtist"}; - ScopedArtist artist6 {session, "%AMyArtist"}; + ScopedArtist artist4{ session, "MyArtist%foo" }; + ScopedArtist artist5{ session, "foo%MyArtist" }; + ScopedArtist artist6{ session, "%AMyArtist" }; - { - auto transaction {session.createSharedTransaction()}; - { - const auto artists {Artist::find(session, "MyArtist%")}; - ASSERT_TRUE(artists.size() == 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); - } - { - const auto artists {Artist::find(session, "%MyArtist")}; - ASSERT_TRUE(artists.size() == 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); - EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); - } - { - const auto artists {Artist::find(session, "%_MyArtist")}; - ASSERT_TRUE(artists.size() == 1); - ASSERT_EQ(artists.front()->getId(), artist3.getId()); - EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); - } - } + { + auto transaction{ session.createSharedTransaction() }; + { + const auto artists{ Artist::find(session, "MyArtist%") }; + ASSERT_TRUE(artists.size() == 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); + } + { + const auto artists{ Artist::find(session, "%MyArtist") }; + ASSERT_TRUE(artists.size() == 1); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); + EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); + } + { + const auto artists{ Artist::find(session, "%_MyArtist") }; + ASSERT_TRUE(artists.size() == 1); + ASSERT_EQ(artists.front()->getId(), artist3.getId()); + EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); + } + } - { - auto transaction {session.createSharedTransaction()}; - { - const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))}; - EXPECT_EQ(artists.results.size(), 6); - } + { + auto transaction{ session.createSharedTransaction() }; + { + const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) }; + EXPECT_EQ(artists.results.size(), 6); + } - { - const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))}; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist1.getId()); - EXPECT_EQ(artists.results[1], artist4.getId()); - } + { + const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) }; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist1.getId()); + EXPECT_EQ(artists.results[1], artist4.getId()); + } - { - const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist2.getId()); - EXPECT_EQ(artists.results[1], artist5.getId()); - } + { + const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2.getId()); + EXPECT_EQ(artists.results[1], artist5.getId()); + } - { - const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist3.getId()); - } - } + { + const auto artists{ Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist3.getId()); + } + } } TEST_F(DatabaseFixture, Artist_sortMethod) { - ScopedArtist artistA {session, "artistA"}; - ScopedArtist artistB {session, "artistB"}; + ScopedArtist artistA{ session, "artistA" }; + ScopedArtist artistB{ session, "artistB" }; - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - artistA.get().modify()->setSortName("sortNameB"); - artistB.get().modify()->setSortName("sortNameA"); - } + artistA.get().modify()->setSortName("sortNameB"); + artistB.get().modify()->setSortName("sortNameA"); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))}; - auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; + auto allArtistsByName{ Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) }; + auto allArtistsBySortName{ Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; - ASSERT_EQ(allArtistsByName.results.size(), 2); - EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); - EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); + ASSERT_EQ(allArtistsByName.results.size(), 2); + EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); + EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); - ASSERT_EQ(allArtistsBySortName.results.size(), 2); - EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); - EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); - } + ASSERT_EQ(allArtistsBySortName.results.size(), 2); + EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); + EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); + } } TEST_F(DatabaseFixture, Artist_nonReleaseTracks) { - ScopedArtist artist {session, "artist"}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedRelease release{session, "MyRelease"}; + ScopedArtist artist{ session, "artist" }; + ScopedTrack track1{ session, "MyTrack1" }; + ScopedTrack track2{ session, "MyTrack2" }; + ScopedRelease release{ session, "MyRelease" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::find(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId()))}; - EXPECT_EQ(tracks.results.size(), 0); - } + auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) }; + EXPECT_EQ(tracks.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); - track1.get().modify()->setRelease(release.get()); - } + track1.get().modify()->setRelease(release.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))}; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track2.getId()); - } + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) }; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track2.getId()); + } } TEST_F(DatabaseFixture, Artist_findByRelease) { - ScopedArtist artist {session, "artist"}; - ScopedTrack track {session, "MyTrack"}; - ScopedRelease release{session, "MyRelease"}; + ScopedArtist artist{ session, "artist" }; + ScopedTrack track{ session, "MyTrack" }; + ScopedRelease release{ session, "MyRelease" }; - { - auto transaction {session.createSharedTransaction()}; - const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; - EXPECT_EQ(artists.results.size(), 0); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) }; + EXPECT_EQ(artists.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - } + { + auto transaction{ session.createUniqueTransaction() }; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } - { - auto transaction {session.createSharedTransaction()}; - const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; - EXPECT_EQ(artists.results.size(), 0); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) }; + EXPECT_EQ(artists.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; - track.get().modify()->setRelease(release.get()); - } + { + auto transaction{ session.createUniqueTransaction() }; + track.get().modify()->setRelease(release.get()); + } - { - auto transaction {session.createSharedTransaction()}; - const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))}; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto artists{ Artist::find(session, Artist::FindParameters {}.setRelease(release.getId())) }; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); + } } - diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index 5e0cbcd0..c914ecfa 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -133,11 +133,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()})) }; + auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) }; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::find(session, Track::FindParameters{}.setClusters({ cluster2.getId() })); + tracks = Track::findIds(session, Track::FindParameters{}.setClusters({ cluster2.getId() })); EXPECT_TRUE(tracks.results.empty()); } @@ -167,7 +167,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) { auto transaction{ session.createSharedTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; + const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; EXPECT_TRUE(tracks.results.empty()); } @@ -180,7 +180,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) { auto transaction{ session.createSharedTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; + const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; EXPECT_TRUE(tracks.results.empty()); } @@ -193,7 +193,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) { auto transaction{ session.createSharedTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters{}.setClusters(clusterIds)) }; + const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; ASSERT_FALSE(tracks.results.empty()); EXPECT_EQ(tracks.results.front(), track.getId()); } @@ -550,7 +550,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) { auto transaction{ session.createSharedTransaction() }; - const auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; + const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); for (const TrackId similarTrackId : similarTracks.results) { @@ -591,14 +591,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) auto transaction{ session.createSharedTransaction() }; { - auto similarTracks{ Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4}) }; + auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.back().getId()}, Range {0, 4}) }; EXPECT_EQ(similarTracks.results.size(), 4); for (const TrackId similarTrackId : similarTracks.results) EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } { - auto similarTracks{ Track::findSimilarTracks(session, {tracks.front().getId()}, Range {}) }; + auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); for (const TrackId similarTrackId : similarTracks.results) EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index f3b400ea..cb3a35b2 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -78,7 +78,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) auto transaction{ session.createSharedTransaction() }; EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); } @@ -92,18 +92,18 @@ TEST_F(DatabaseFixture, Release_singleTrack) { auto transaction{ session.createUniqueTransaction() }; - auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) }; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); } { auto transaction{ session.createUniqueTransaction() }; - auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) }; EXPECT_EQ(tracks.results.size(), 0); } { auto transaction{ session.createUniqueTransaction() }; - auto tracks{ Track::find(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) }; EXPECT_EQ(tracks.results.size(), 0); } } @@ -111,7 +111,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) { auto transaction{ session.createUniqueTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release.getId())) }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; EXPECT_TRUE(tracks.results.empty()); auto releases{ Release::findOrphanIds(session, Range {}) }; @@ -285,8 +285,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Track::find(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty()); - EXPECT_TRUE(Track::find(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty()); + EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty()); + EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty()); } { @@ -310,13 +310,13 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) auto transaction{ session.createSharedTransaction() }; { - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; ASSERT_FALSE(tracks.results.empty()); EXPECT_EQ(tracks.results.front(), track1A.getId()); } { - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; ASSERT_FALSE(tracks.results.empty()); EXPECT_EQ(tracks.results.front(), track2B.getId()); } diff --git a/src/libs/services/database/test/StarredTrack.cpp b/src/libs/services/database/test/StarredTrack.cpp index 2c783679..a471215e 100644 --- a/src/libs/services/database/test/StarredTrack.cpp +++ b/src/libs/services/database/test/StarredTrack.cpp @@ -37,7 +37,7 @@ TEST_F(DatabaseFixture, StarredTrack) EXPECT_FALSE(starredTrack); EXPECT_EQ(StarredTrack::getCount(session), 0); - auto tracks {Track::find(session, Track::FindParameters {})}; + auto tracks {Track::findIds(session, Track::FindParameters {})}; EXPECT_EQ(tracks.results.size(), 1); } @@ -53,13 +53,13 @@ TEST_F(DatabaseFixture, StarredTrack) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {})}; + auto tracks {Track::findIds(session, Track::FindParameters {})}; EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); + tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); + tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); EXPECT_EQ(tracks.results.size(), 0); } } @@ -73,11 +73,11 @@ TEST_F(DatabaseFixture, Starredtrack_PendingDestroy) { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; EXPECT_EQ(tracks.results.size(), 1); starredTrack.get().modify()->setSyncState(SyncState::PendingRemove); - tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); + tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); EXPECT_EQ(tracks.results.size(), 0); } } @@ -96,7 +96,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; EXPECT_EQ(tracks.results.size(), 2); } @@ -106,7 +106,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime) starredTrack1.get().modify()->setDateTime(dateTime); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); @@ -117,7 +117,7 @@ TEST_F(DatabaseFixture, StarredTrack_dateTime) starredTrack1.get().modify()->setDateTime(dateTime); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))}; ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); diff --git a/src/libs/services/database/test/Track.cpp b/src/libs/services/database/test/Track.cpp index 3e959a70..0711edb6 100644 --- a/src/libs/services/database/test/Track.cpp +++ b/src/libs/services/database/test/Track.cpp @@ -25,137 +25,138 @@ using namespace Database; TEST_F(DatabaseFixture, Track) { - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0); - EXPECT_EQ(Track::getCount(session), 0); - EXPECT_FALSE(Track::exists(session, 0)); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0); + EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_FALSE(Track::exists(session, 0)); + } - ScopedTrack track {session, "MyTrackFile"}; + ScopedTrack track{ session, "MyTrackFile" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1); - EXPECT_EQ(Track::getCount(session), 1); - EXPECT_TRUE(Track::exists(session, track.getId())); - auto myTrack {Track::find(session, track.getId())}; - ASSERT_TRUE(myTrack); - EXPECT_EQ(myTrack->getId(), track.getId()); - } + EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1); + EXPECT_EQ(Track::getCount(session), 1); + EXPECT_TRUE(Track::exists(session, track.getId())); + auto myTrack{ Track::find(session, track.getId()) }; + ASSERT_TRUE(myTrack); + EXPECT_EQ(myTrack->getId(), track.getId()); + } } TEST_F(DatabaseFixture, MultipleTracks) { - ScopedTrack track1 {session, "MyTrackFile1"}; - ScopedTrack track2 {session, "MyTrackFile2"}; + ScopedTrack track1{ session, "MyTrackFile1" }; + ScopedTrack track2{ session, "MyTrackFile2" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(track1.getId() != track2.getId()); - EXPECT_TRUE(track1.get() != track2.get()); - EXPECT_FALSE(track1.get() == track2.get()); - } + EXPECT_TRUE(track1.getId() != track2.getId()); + EXPECT_TRUE(track1.get() != track2.get()); + EXPECT_FALSE(track1.get() == track2.get()); + } } TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) { - ScopedTrack track1 {session, ""}; - ScopedTrack track2 {session, ""}; - ScopedTrack track3 {session, ""}; - ScopedTrack track4 {session, ""}; - ScopedTrack track5 {session, ""}; - ScopedTrack track6 {session, ""}; + ScopedTrack track1{ session, "" }; + ScopedTrack track2{ session, "" }; + ScopedTrack track3{ session, "" }; + ScopedTrack track4{ session, "" }; + ScopedTrack track5{ session, "" }; + ScopedTrack track6{ session, "" }; - { - auto transaction {session.createUniqueTransaction()}; - track1.get().modify()->setName("MyTrack"); - track2.get().modify()->setName("MyTrack%"); - track3.get().modify()->setName("MyTrack%Foo"); - track4.get().modify()->setName("%MyTrack"); - track5.get().modify()->setName("Foo%MyTrack"); - track6.get().modify()->setName("M_Track"); - } + { + auto transaction{ session.createUniqueTransaction() }; + track1.get().modify()->setName("MyTrack"); + track2.get().modify()->setName("MyTrack%"); + track3.get().modify()->setName("MyTrack%Foo"); + track4.get().modify()->setName("%MyTrack"); + track5.get().modify()->setName("Foo%MyTrack"); + track6.get().modify()->setName("M_Track"); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - { - const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))}; - EXPECT_EQ(tracks.results.size(), 6); - } - { - const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))}; - EXPECT_EQ(tracks.results.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); - } - { - const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))}; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2.getId()); - EXPECT_EQ(tracks.results[1], track3.getId()); - } - { - const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))}; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track4.getId()); - EXPECT_EQ(tracks.results[1], track5.getId()); - } - } + { + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) }; + EXPECT_EQ(tracks.results.size(), 6); + } + { + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack"})) }; + EXPECT_EQ(tracks.results.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); + } + { + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"MyTrack%"})) }; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track3.getId()); + } + { + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"%MyTrack"})) }; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track4.getId()); + EXPECT_EQ(tracks.results[1], track5.getId()); + } + } } TEST_F(DatabaseFixture, Track_date) { - ScopedTrack track {session, "MyTrack"}; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(track->getYear(), std::nullopt); - EXPECT_EQ(track->getOriginalYear(), std::nullopt); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(track->getYear(), std::nullopt); + EXPECT_EQ(track->getOriginalYear(), std::nullopt); + } - { - auto transaction {session.createUniqueTransaction()}; - track.get().modify()->setDate(Wt::WDate {1995, 5, 5}); - track.get().modify()->setOriginalDate(Wt::WDate {1994, 2, 2}); - } + { + auto transaction{ session.createUniqueTransaction() }; + track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 }); + track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 }); + } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(track->getYear(), 1995); - EXPECT_EQ(track->getOriginalYear(), 1994); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(track->getYear(), 1995); + EXPECT_EQ(track->getOriginalYear(), 1994); + } } TEST_F(DatabaseFixture, Track_writtenAfter) { - ScopedTrack track {session, "MyTrack"}; + ScopedTrack track{ session, "MyTrack" }; - const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; + const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} }; - { - auto transaction {session.createUniqueTransaction()}; - track.get().modify()->setLastWriteTime(dateTime); - } + { + auto transaction{ session.createUniqueTransaction() }; + track.get().modify()->setLastWriteTime(dateTime); + } - { - auto transaction {session.createSharedTransaction()}; - const auto tracks {Track::find(session, Track::FindParameters {})}; - EXPECT_EQ(tracks.results.size(), 1); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}) }; + EXPECT_EQ(tracks.results.size(), 1); + } - { - auto transaction {session.createSharedTransaction()}; - const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; - EXPECT_EQ(tracks.results.size(), 1); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) }; + EXPECT_EQ(tracks.results.size(), 1); + } - { - auto transaction {session.createSharedTransaction()}; - const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; - EXPECT_EQ(tracks.results.size(), 0); - } + { + auto transaction{ session.createSharedTransaction() }; + const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) }; + EXPECT_EQ(tracks.results.size(), 0); + } } diff --git a/src/libs/services/database/test/TrackList.cpp b/src/libs/services/database/test/TrackList.cpp index cb2db0c9..f05cb48a 100644 --- a/src/libs/services/database/test/TrackList.cpp +++ b/src/libs/services/database/test/TrackList.cpp @@ -49,10 +49,10 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; EXPECT_EQ(tracks.results.size(), 0); - tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); + tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); EXPECT_EQ(tracks.results.size(), 0); } @@ -65,11 +65,11 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; + auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::find(session, Track::FindParameters {}.setTrackList(trackList2.getId())); + tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); EXPECT_EQ(tracks.results.size(), 0); } } diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp index 30981a8b..cf414819 100644 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -179,7 +179,7 @@ namespace Feedback Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Track::find(session, params); + return Track::findIds(session, params); } } // ns Feedback diff --git a/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp b/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp index b136e9b4..6e235aea 100644 --- a/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp +++ b/src/libs/services/recommendation/impl/PlaylistGeneratorService.cpp @@ -30,89 +30,86 @@ namespace Recommendation { - using namespace Database; + using namespace Database; - std::unique_ptr - createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) - { - return std::make_unique(db, recommendationService); - } + std::unique_ptr createPlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) + { + return std::make_unique(db, recommendationService); + } - PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) - : _db {db} - , _recommendationService {recommendationService} - { - _constraints.push_back(std::make_unique(_db)); - _constraints.push_back(std::make_unique(_db)); - _constraints.push_back(std::make_unique()); - } + PlaylistGeneratorService::PlaylistGeneratorService(Db& db, Recommendation::IRecommendationService& recommendationService) + : _db{ db } + , _recommendationService{ recommendationService } + { + _constraints.push_back(std::make_unique(_db)); + _constraints.push_back(std::make_unique(_db)); + _constraints.push_back(std::make_unique()); + } - std::vector - PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const - { - LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks"; + std::vector PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks"; - // supposed to be ordered from most similar to least similar - std::vector similarTracks {_recommendationService.findSimilarTracks(tracklistId, maxCount * 2)}; // ask for more tracks than we need as it will be easier to respect constraints + // supposed to be ordered from most similar to least similar + std::vector similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints - const std::vector startingTracks {getTracksFromTrackList(tracklistId)}; + const std::vector startingTracks{ getTracksFromTrackList(tracklistId) }; - std::vector finalResult = startingTracks; - finalResult.reserve(startingTracks.size() + maxCount); + std::vector finalResult = startingTracks; + finalResult.reserve(startingTracks.size() + maxCount); - std::vector scores; - for (std::size_t i {}; i < maxCount; ++i) - { - if (similarTracks.empty()) - break; + std::vector scores; + for (std::size_t i{}; i < maxCount; ++i) + { + if (similarTracks.empty()) + break; - scores.resize(similarTracks.size(), {}); + scores.resize(similarTracks.size(), {}); - // select the similar track that has the best score - for (std::size_t trackIndex {}; trackIndex < similarTracks.size(); ++trackIndex) - { - using namespace Database::Debug; + // select the similar track that has the best score + for (std::size_t trackIndex{}; trackIndex < similarTracks.size(); ++trackIndex) + { + using namespace Database::Debug; - finalResult.push_back(similarTracks[trackIndex]); + finalResult.push_back(similarTracks[trackIndex]); - scores[trackIndex] = 0; - for (const auto& constraint : _constraints) - scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1); + scores[trackIndex] = 0; + for (const auto& constraint : _constraints) + scores[trackIndex] += constraint->computeScore(finalResult, finalResult.size() - 1); - finalResult.pop_back(); + finalResult.pop_back(); - // early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar) - if (scores[trackIndex] < 0.01) - break; - } + // early exit if we consider we found a track with no constraint violation (since similarTracks sorted from most to least similar) + if (scores[trackIndex] < 0.01) + break; + } - // get the best score - const std::size_t bestScoreIndex {static_cast(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores))))}; + // get the best score + const std::size_t bestScoreIndex{ static_cast(std::distance(std::cbegin(scores), std::min_element(std::cbegin(scores), std::cend(scores)))) }; - finalResult.push_back(similarTracks[bestScoreIndex]); - similarTracks.erase(std::begin(similarTracks) + bestScoreIndex); - } + finalResult.push_back(similarTracks[bestScoreIndex]); + similarTracks.erase(std::begin(similarTracks) + bestScoreIndex); + } - // for now, just get some more similar tracks - return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult)); - } + // for now, just get some more similar tracks + return std::vector(std::cbegin(finalResult) + startingTracks.size(), std::cend(finalResult)); + } - TrackContainer - PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const - { - TrackContainer tracks; + TrackContainer PlaylistGeneratorService::getTracksFromTrackList(Database::TrackListId tracklistId) const + { + TrackContainer tracks; - Session& dbSession {_db.getTLSSession()}; - auto transaction {dbSession.createSharedTransaction()}; + Session& dbSession{ _db.getTLSSession() }; + auto transaction{ dbSession.createSharedTransaction() }; - Track::FindParameters params; - params.setTrackList(tracklistId); - params.setSortMethod(TrackSortMethod::TrackList); - params.setDistinct(false); + Track::FindParameters params; + params.setTrackList(tracklistId); + params.setSortMethod(TrackSortMethod::TrackList); + params.setDistinct(false); - for (const TrackId trackId : Track::find(dbSession, params).results) - tracks.push_back(trackId); + for (const TrackId trackId : Track::findIds(dbSession, params).results) + tracks.push_back(trackId); - return tracks; - } + return tracks; + } } diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp index ce8abc37..dd11272f 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -44,7 +44,7 @@ namespace Recommendation { Session& dbSession{ _db.getTLSSession() }; auto transaction{ dbSession.createSharedTransaction() }; - const auto similarTrackIds{ Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount}) }; + const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) }; return std::move(similarTrackIds.results); } diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp index 8d1a0773..0e18f4a5 100644 --- a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp @@ -37,7 +37,7 @@ namespace Scanner Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const RangeResults tracks = Track::findTrackMBIDDuplicates(session, Range {}); + const RangeResults tracks = Track::findIdsTrackMBIDDuplicates(session, Range {}); for (const TrackId trackId : tracks.results) { const Track::pointer track {Track::find(session, trackId)}; diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index 84cd6b85..b72a5238 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -42,549 +42,530 @@ namespace { - using namespace Scrobbling::ListenBrainz; + using namespace Scrobbling::ListenBrainz; - std::optional - listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) - { - auto transaction {session.createSharedTransaction()}; + std::optional listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) + { + auto transaction{ session.createSharedTransaction() }; - const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; - if (!track) - return std::nullopt; + const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) }; + if (!track) + return std::nullopt; - auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; - if (artists.empty()) - artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist}); + auto artists{ track->getArtists({Database::TrackArtistLinkType::Artist}) }; + if (artists.empty()) + artists = track->getArtists({ Database::TrackArtistLinkType::ReleaseArtist }); - if (artists.empty()) - { - LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist"; - return std::nullopt; - } + if (artists.empty()) + { + LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist"; + return std::nullopt; + } - Wt::Json::Object additionalInfo; - additionalInfo["listening_from"] = "LMS"; - if (track->getRelease()) - { - if (auto MBID {track->getRelease()->getMBID()}) - additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; - } + Wt::Json::Object additionalInfo; + additionalInfo["listening_from"] = "LMS"; + if (track->getRelease()) + { + if (auto MBID{ track->getRelease()->getMBID() }) + additionalInfo["release_mbid"] = Wt::Json::Value{ std::string {MBID->getAsString()} }; + } - { - Wt::Json::Array artistMBIDs; - for (const Database::Artist::pointer& artist : artists) - { - if (auto MBID {artist->getMBID()}) - artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}}); - } + { + Wt::Json::Array artistMBIDs; + for (const Database::Artist::pointer& artist : artists) + { + if (auto MBID{ artist->getMBID() }) + artistMBIDs.push_back(Wt::Json::Value{ std::string {MBID->getAsString()} }); + } - if (!artistMBIDs.empty()) - additionalInfo["artist_mbids"] = std::move(artistMBIDs); - } + if (!artistMBIDs.empty()) + additionalInfo["artist_mbids"] = std::move(artistMBIDs); + } - if (auto MBID {track->getTrackMBID()}) - additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; + if (auto MBID{ track->getTrackMBID() }) + additionalInfo["track_mbid"] = Wt::Json::Value{ std::string {MBID->getAsString()} }; - if (auto MBID {track->getRecordingMBID()}) - additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}}; + if (auto MBID{ track->getRecordingMBID() }) + additionalInfo["recording_mbid"] = Wt::Json::Value{ std::string {MBID->getAsString()} }; - if (const std::optional trackNumber {track->getTrackNumber()}) - additionalInfo["tracknumber"] = Wt::Json::Value {static_cast(*trackNumber)}; + if (const std::optional trackNumber{ track->getTrackNumber() }) + additionalInfo["tracknumber"] = Wt::Json::Value{ static_cast(*trackNumber) }; - Wt::Json::Object trackMetadata; - trackMetadata["additional_info"] = std::move(additionalInfo); - trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()}; - trackMetadata["track_name"] = Wt::Json::Value {track->getName()}; - if (track->getRelease()) - trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()}; + Wt::Json::Object trackMetadata; + trackMetadata["additional_info"] = std::move(additionalInfo); + trackMetadata["artist_name"] = Wt::Json::Value{ artists.front()->getName() }; + trackMetadata["track_name"] = Wt::Json::Value{ track->getName() }; + if (track->getRelease()) + trackMetadata["release_name"] = Wt::Json::Value{ track->getRelease()->getName() }; - Wt::Json::Object payload; - payload["track_metadata"] = std::move(trackMetadata); - if (timePoint.isValid()) - payload["listened_at"] = Wt::Json::Value {static_cast(timePoint.toTime_t())}; + Wt::Json::Object payload; + payload["track_metadata"] = std::move(trackMetadata); + if (timePoint.isValid()) + payload["listened_at"] = Wt::Json::Value{ static_cast(timePoint.toTime_t()) }; - return payload; - } + return payload; + } - std::string - listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType) - { - std::string res; + std::string listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType) + { + std::string res; - std::optional payload {listenToJsonPayload(session, listen, timePoint)}; - if (!payload) - return res; + std::optional payload{ listenToJsonPayload(session, listen, timePoint) }; + if (!payload) + return res; - Wt::Json::Object root; - root["listen_type"] = Wt::Json::Value {std::string {listenType}}; - root["payload"] = Wt::Json::Array {std::move(*payload)}; + Wt::Json::Object root; + root["listen_type"] = Wt::Json::Value{ std::string {listenType} }; + root["payload"] = Wt::Json::Array{ std::move(*payload) }; - res = Wt::Json::serialize(root); - return res; - } + res = Wt::Json::serialize(root); + return res; + } - std::optional - parseListenCount(std::string_view msgBody) - { - try - { - Wt::Json::Object root; - Wt::Json::parse(std::string {msgBody}, root); + std::optional parseListenCount(std::string_view msgBody) + { + try + { + Wt::Json::Object root; + Wt::Json::parse(std::string{ msgBody }, root); - const Wt::Json::Object& payload {static_cast(root.get("payload"))}; - return static_cast(payload.get("count")); - } - catch (const Wt::WException& e) - { - LOG(ERROR) << "Cannot parse listen count response: " << e.what(); - return std::nullopt; - } - } + const Wt::Json::Object& payload{ static_cast(root.get("payload")) }; + return static_cast(payload.get("count")); + } + catch (const Wt::WException& e) + { + LOG(ERROR) << "Cannot parse listen count response: " << e.what(); + return std::nullopt; + } + } - Database::TrackId - tryGetMatchingTrack(Database::Session& session, const Listen& listen) - { - using namespace Database; + Database::TrackId tryGetMatchingTrack(Database::Session& session, const Listen& listen) + { + using namespace Database; - auto transaction {session.createSharedTransaction()}; + auto transaction{ session.createSharedTransaction() }; - // first try to match using track MBID, and then fallback on possibly ambiguous info - if (listen.trackMBID) - { - const auto tracks {Track::findByMBID(session, *listen.trackMBID)}; - // if duplicated files, do not record it (let the user correct its database) - if (tracks.size() == 1) - { - LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID"; - return tracks.front()->getId(); - } - else if (tracks.size() > 1) - { - LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!"; - return {}; - } - } + // first try to match using track MBID, and then fallback on possibly ambiguous info + if (listen.trackMBID) + { + const auto tracks{ Track::findByMBID(session, *listen.trackMBID) }; + // if duplicated files, do not record it (let the user correct its database) + if (tracks.size() == 1) + { + LOG(DEBUG) << "Matched listen '" << listen << "' using track MBID"; + return tracks.front()->getId(); + } + else if (tracks.size() > 1) + { + LOG(DEBUG) << "Too many matches for listen '" << listen << "' using track MBID!"; + return {}; + } + } - if (listen.recordingMBID) - { - const auto tracks {Track::findByRecordingMBID(session, *listen.recordingMBID)}; - // if duplicated files, do not record it (let the user correct its database) - if (tracks.size() == 1) - { - LOG(DEBUG) << "Matched listen '" << listen << "' using recording MBID"; - return tracks.front()->getId(); - } - else if (tracks.size() > 1) - { - LOG(DEBUG) << "Too many matches for listen '" << listen << "' using recording MBID!"; - return {}; - } - } + if (listen.recordingMBID) + { + const auto tracks{ Track::findByRecordingMBID(session, *listen.recordingMBID) }; + // if duplicated files, do not record it (let the user correct its database) + if (tracks.size() == 1) + { + LOG(DEBUG) << "Matched listen '" << listen << "' using recording MBID"; + return tracks.front()->getId(); + } + else if (tracks.size() > 1) + { + LOG(DEBUG) << "Too many matches for listen '" << listen << "' using recording MBID!"; + return {}; + } + } - assert(!listen.trackName.empty() && !listen.artistName.empty()); + assert(!listen.trackName.empty() && !listen.artistName.empty()); - // TODO check release MBID? - Track::FindParameters params; - params.setName(listen.trackName); - params.setReleaseName(listen.releaseName); - params.setArtistName(listen.artistName); - if (listen.trackNumber) - params.setTrackNumber(*listen.trackNumber); + // TODO check release MBID? + Track::FindParameters params; + params.setName(listen.trackName); + params.setReleaseName(listen.releaseName); + params.setArtistName(listen.artistName); + if (listen.trackNumber) + params.setTrackNumber(*listen.trackNumber); - const auto tracks {Track::find(session, params)}; - // conservative behavior: in case of multiple matches: reject - if (tracks.results.size() == 1) - { - LOG(DEBUG) << "Matched listen '" << listen << "' using metadata"; - return tracks.results.front(); - } - else if (tracks.results.size() > 1) - { - LOG(DEBUG) << "Too many matches for listen '" << listen << "' using metadata"; - return {}; - } + const auto tracks{ Track::findIds(session, params) }; + // conservative behavior: in case of multiple matches: reject + if (tracks.results.size() == 1) + { + LOG(DEBUG) << "Matched listen '" << listen << "' using metadata"; + return tracks.results.front(); + } + else if (tracks.results.size() > 1) + { + LOG(DEBUG) << "Too many matches for listen '" << listen << "' using metadata"; + return {}; + } - LOG(DEBUG) << "No match for listen '" << listen << "'"; - return {}; - } + LOG(DEBUG) << "No match for listen '" << listen << "'"; + return {}; + } } namespace Scrobbling::ListenBrainz { - ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client) - : _ioContext {ioContext} - , _db {db} - , _client {client} - , _maxSyncListenCount {Service::get()->getULong("listenbrainz-max-sync-listen-count", 1000)} - , _syncListensPeriod {Service::get()->getULong("listenbrainz-sync-listens-period-hours", 1)} - { - LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours"; - - scheduleSync(std::chrono::seconds {30}); - } - - void - ListensSynchronizer::enqueListen(const TimedListen& listen) - { - assert(listen.listenedAt.isValid()); - enqueListen(listen, listen.listenedAt); - } - - void - ListensSynchronizer::enqueListenNow(const Scrobbling::Listen& listen) - { - enqueListen(listen, {}); - } - - void - ListensSynchronizer::enqueListen(const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) - { - Http::ClientPOSTRequestParameters request; - request.relativeUrl = "/1/submit-listens"; - - if (timePoint.isValid()) - { - const TimedListen timedListen {listen, timePoint}; - // We want the listen to be sent again later in case of failure, so we just save it as pending send - saveListen(timedListen, Database::SyncState::PendingAdd); - - request.priority = Http::ClientRequestParameters::Priority::Normal; - request.onSuccessFunc = [=](std::string_view) - { - _strand.dispatch([=] - { - if (saveListen(timedListen, Database::SyncState::Synchronized)) - { - UserContext& context {getUserContext(listen.userId)}; - if (context.listenCount) - (*context.listenCount)++; - } - }); - }; - // on failure, this listen will be sent during the next sync - } - else - { - // We want "listen now" to appear as soon as possible - request.priority = Http::ClientRequestParameters::Priority::High; - // don't retry on failure - } - - std::string bodyText {listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now")}; - if (bodyText.empty()) - { - LOG(DEBUG) << "Cannot convert listen to json: skipping"; - return; - } - - const std::optional listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId)}; - if (!listenBrainzToken) - { - LOG(DEBUG) << "No listenbrainz token found: skipping"; - return; - } - - request.message.addBodyText(bodyText); - request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()}); - request.message.addHeader("Content-Type", "application/json"); - _client.sendPOSTRequest(std::move(request)); - } - - bool - ListensSynchronizer::saveListen(const TimedListen& listen, Database::SyncState scrobblingState) - { - using namespace Database; - - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; // TODO: unique only if needed - - Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt)}; - if (!dbListen) - { - const User::pointer user {User::find(session, listen.userId)}; - if (!user) - return false; - - const Track::pointer track {Track::find(session, listen.trackId)}; - if (!track) - return false; - - dbListen = session.create(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt); - dbListen.modify()->setSyncState(scrobblingState); - - LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); - - return true; - } - - if (dbListen->getSyncState() == scrobblingState) - return false; - - dbListen.modify()->setSyncState(scrobblingState); - return true; - } - - void - ListensSynchronizer::enquePendingListens() - { - std::vector pendingListens; - - { - Database::Session& session {_db.getTLSSession()}; - - auto transaction {session.createUniqueTransaction()}; - - Database::Listen::FindParameters params; - params.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz) - .setSyncState(Database::SyncState::PendingAdd) - .setRange(Database::Range {0, 100}); // don't flood too much? - - const Database::RangeResults results {Database::Listen::find(session, params)}; - pendingListens.reserve(results.results.size()); - - for (Database::ListenId listenId : results.results) - { - const Database::Listen::pointer listen {Database::Listen::find(session, listenId)}; - - TimedListen timedListen; - timedListen.listenedAt = listen->getDateTime(); - timedListen.userId = listen->getUser()->getId(); - timedListen.trackId = listen->getTrack()->getId(); - - pendingListens.push_back(std::move(timedListen)); - } - } - - LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen"; - - for (const TimedListen& pendingListen : pendingListens) - enqueListen(pendingListen); - } - - ListensSynchronizer::UserContext& - ListensSynchronizer::getUserContext(Database::UserId userId) - { - assert(_strand.running_in_this_thread()); - - auto itContext {_userContexts.find(userId)}; - if (itContext == std::cend(_userContexts)) - { - std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId); - } - - return itContext->second; - } - - bool - ListensSynchronizer::isSyncing() const - { - return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) - { - return contextEntry.second.syncing; - }); - } - - void - ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow) - { - if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0) - return; - - LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; - _syncTimer.expires_after(fromNow); - _syncTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec) - { - if (ec == boost::asio::error::operation_aborted) - { - LOG(DEBUG) << "getListens aborted"; - return; - } - else if (ec) - { - throw Exception {"GetListens timer failure: " + std::string {ec.message()} }; - } - - startSync(); - })); - } - - void - ListensSynchronizer::startSync() - { - LOG(DEBUG) << "Starting sync!"; - - assert(!isSyncing()); - - enquePendingListens(); - - Database::RangeResults userIds; - { - Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz)); - } - - for (const Database::UserId userId : userIds.results) - startSync(getUserContext(userId)); - - if (!isSyncing()) - scheduleSync(_syncListensPeriod); - } - - void - ListensSynchronizer::startSync(UserContext& context) - { - context.syncing = true; - context.listenBrainzUserName = ""; - context.maxDateTime = {}; - context.fetchedListenCount = 0; - context.matchedListenCount = 0; - context.importedListenCount = 0; - - enqueValidateToken(context); - } - - void - ListensSynchronizer::onSyncEnded(UserContext& context) - { - _strand.dispatch([this, &context] - { - LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount; - context.syncing = false; - - if (!isSyncing()) - scheduleSync(_syncListensPeriod); - }); - } - - void - ListensSynchronizer::enqueValidateToken(UserContext& context) - { - assert(context.listenBrainzUserName.empty()); - - const std::optional listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)}; - if (!listenBrainzToken) - { - onSyncEnded(context); - return; - } - - Http::ClientGETRequestParameters request; - request.priority = Http::ClientRequestParameters::Priority::Low; - request.relativeUrl = "/1/validate-token"; - request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} }; - request.onSuccessFunc = [this, &context] (std::string_view msgBody) - { - context.listenBrainzUserName = Utils::parseValidateToken(msgBody); - if (context.listenBrainzUserName.empty()) - { - onSyncEnded(context); - return; - } - enqueGetListenCount(context); - }; - request.onFailureFunc = [this, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - void - ListensSynchronizer::enqueGetListenCount(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - Http::ClientGETRequestParameters request; - request.relativeUrl = "/1/user/" + std::string {context.listenBrainzUserName} + "/listen-count"; - request.priority = Http::ClientRequestParameters::Priority::Low; - request.onSuccessFunc = [=, &context] (std::string_view msgBody) - { - _strand.dispatch([=, &context] - { - const auto listenCount = parseListenCount(msgBody); - if (listenCount) - LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount; - - bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)}; - context.listenCount = listenCount; - - if (!needSync) - { - onSyncEnded(context); - return; - } - - context.maxDateTime = Wt::WDateTime::currentDateTime(); - enqueGetListens(context); - }); - }; - request.onFailureFunc = [this, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - void - ListensSynchronizer::enqueGetListens(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - Http::ClientGETRequestParameters request; - request.relativeUrl = "/1/user/" + context.listenBrainzUserName + "/listens?max_ts=" + std::to_string(context.maxDateTime.toTime_t()); - request.priority = Http::ClientRequestParameters::Priority::Low; - request.onSuccessFunc = [=, &context] (std::string_view msgBody) - { - processGetListensResponse(msgBody, context); - if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid()) - { - onSyncEnded(context); - return; - } - - enqueGetListens(context); - }; - request.onFailureFunc = [=, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - void - ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context) - { - Database::Session& session {_db.getTLSSession()}; - - context.maxDateTime = {}; // invalidate to break in case no more listens are fetched - ListensParser::Result result {ListensParser::parse(msgBody)}; - context.fetchedListenCount += result.listenCount; - - for (const Listen& parsedListen : result.listens) - { - // update oldest listen for the next query - if (!parsedListen.listenedAt.isValid()) - { - LOG(DEBUG) << "Skipping entry due to invalid listenedAt"; - continue; - } - - if (!context.maxDateTime.isValid() || context.maxDateTime > parsedListen.listenedAt) - context.maxDateTime = parsedListen.listenedAt; - - if (const Database::TrackId trackId {tryGetMatchingTrack(session, parsedListen)}; trackId.isValid()) - { - context.matchedListenCount++; - - const Scrobbling::TimedListen listen {{context.userId, trackId}, parsedListen.listenedAt}; - if (saveListen(listen, Database::SyncState::Synchronized)) - context.importedListenCount++; - } - } - } + ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client) + : _ioContext{ ioContext } + , _db{ db } + , _client{ client } + , _maxSyncListenCount{ Service::get()->getULong("listenbrainz-max-sync-listen-count", 1000) } + , _syncListensPeriod{ Service::get()->getULong("listenbrainz-sync-listens-period-hours", 1) } + { + LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours"; + + scheduleSync(std::chrono::seconds{ 30 }); + } + + void ListensSynchronizer::enqueListen(const TimedListen& listen) + { + assert(listen.listenedAt.isValid()); + enqueListen(listen, listen.listenedAt); + } + + void ListensSynchronizer::enqueListenNow(const Scrobbling::Listen& listen) + { + enqueListen(listen, {}); + } + + void ListensSynchronizer::enqueListen(const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint) + { + Http::ClientPOSTRequestParameters request; + request.relativeUrl = "/1/submit-listens"; + + if (timePoint.isValid()) + { + const TimedListen timedListen{ listen, timePoint }; + // We want the listen to be sent again later in case of failure, so we just save it as pending send + saveListen(timedListen, Database::SyncState::PendingAdd); + + request.priority = Http::ClientRequestParameters::Priority::Normal; + request.onSuccessFunc = [=](std::string_view) + { + _strand.dispatch([=] + { + if (saveListen(timedListen, Database::SyncState::Synchronized)) + { + UserContext& context{ getUserContext(listen.userId) }; + if (context.listenCount) + (*context.listenCount)++; + } + }); + }; + // on failure, this listen will be sent during the next sync + } + else + { + // We want "listen now" to appear as soon as possible + request.priority = Http::ClientRequestParameters::Priority::High; + // don't retry on failure + } + + std::string bodyText{ listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now") }; + if (bodyText.empty()) + { + LOG(DEBUG) << "Cannot convert listen to json: skipping"; + return; + } + + const std::optional listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) }; + if (!listenBrainzToken) + { + LOG(DEBUG) << "No listenbrainz token found: skipping"; + return; + } + + request.message.addBodyText(bodyText); + request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() }); + request.message.addHeader("Content-Type", "application/json"); + _client.sendPOSTRequest(std::move(request)); + } + + bool ListensSynchronizer::saveListen(const TimedListen& listen, Database::SyncState scrobblingState) + { + using namespace Database; + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; // TODO: unique only if needed + + Database::Listen::pointer dbListen{ Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt) }; + if (!dbListen) + { + const User::pointer user{ User::find(session, listen.userId) }; + if (!user) + return false; + + const Track::pointer track{ Track::find(session, listen.trackId) }; + if (!track) + return false; + + dbListen = session.create(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt); + dbListen.modify()->setSyncState(scrobblingState); + + LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); + + return true; + } + + if (dbListen->getSyncState() == scrobblingState) + return false; + + dbListen.modify()->setSyncState(scrobblingState); + return true; + } + + void ListensSynchronizer::enquePendingListens() + { + std::vector pendingListens; + + { + Database::Session& session{ _db.getTLSSession() }; + + auto transaction{ session.createUniqueTransaction() }; + + Database::Listen::FindParameters params; + params.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz) + .setSyncState(Database::SyncState::PendingAdd) + .setRange(Database::Range{ 0, 100 }); // don't flood too much? + + const Database::RangeResults results{ Database::Listen::find(session, params) }; + pendingListens.reserve(results.results.size()); + + for (Database::ListenId listenId : results.results) + { + const Database::Listen::pointer listen{ Database::Listen::find(session, listenId) }; + + TimedListen timedListen; + timedListen.listenedAt = listen->getDateTime(); + timedListen.userId = listen->getUser()->getId(); + timedListen.trackId = listen->getTrack()->getId(); + + pendingListens.push_back(std::move(timedListen)); + } + } + + LOG(DEBUG) << "Queing " << pendingListens.size() << " pending listen"; + + for (const TimedListen& pendingListen : pendingListens) + enqueListen(pendingListen); + } + + ListensSynchronizer::UserContext& ListensSynchronizer::getUserContext(Database::UserId userId) + { + assert(_strand.running_in_this_thread()); + + auto itContext{ _userContexts.find(userId) }; + if (itContext == std::cend(_userContexts)) + { + std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId); + } + + return itContext->second; + } + + bool ListensSynchronizer::isSyncing() const + { + return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) + { + return contextEntry.second.syncing; + }); + } + + void ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow) + { + if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0) + return; + + LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; + _syncTimer.expires_after(fromNow); + _syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec) + { + if (ec == boost::asio::error::operation_aborted) + { + LOG(DEBUG) << "getListens aborted"; + return; + } + else if (ec) + { + throw Exception{ "GetListens timer failure: " + std::string {ec.message()} }; + } + + startSync(); + })); + } + + void ListensSynchronizer::startSync() + { + LOG(DEBUG) << "Starting sync!"; + + assert(!isSyncing()); + + enquePendingListens(); + + Database::RangeResults userIds; + { + Database::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz)); + } + + for (const Database::UserId userId : userIds.results) + startSync(getUserContext(userId)); + + if (!isSyncing()) + scheduleSync(_syncListensPeriod); + } + + void ListensSynchronizer::startSync(UserContext& context) + { + context.syncing = true; + context.listenBrainzUserName = ""; + context.maxDateTime = {}; + context.fetchedListenCount = 0; + context.matchedListenCount = 0; + context.importedListenCount = 0; + + enqueValidateToken(context); + } + + void ListensSynchronizer::onSyncEnded(UserContext& context) + { + _strand.dispatch([this, &context] + { + LOG(INFO) << "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount; + context.syncing = false; + + if (!isSyncing()) + scheduleSync(_syncListensPeriod); + }); + } + + void ListensSynchronizer::enqueValidateToken(UserContext& context) + { + assert(context.listenBrainzUserName.empty()); + + const std::optional listenBrainzToken{ Utils::getListenBrainzToken(_db.getTLSSession(), context.userId) }; + if (!listenBrainzToken) + { + onSyncEnded(context); + return; + } + + Http::ClientGETRequestParameters request; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.relativeUrl = "/1/validate-token"; + request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} }; + request.onSuccessFunc = [this, &context](std::string_view msgBody) + { + context.listenBrainzUserName = Utils::parseValidateToken(msgBody); + if (context.listenBrainzUserName.empty()) + { + onSyncEnded(context); + return; + } + enqueGetListenCount(context); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void ListensSynchronizer::enqueGetListenCount(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/user/" + std::string{ context.listenBrainzUserName } + "/listen-count"; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [=, &context](std::string_view msgBody) + { + _strand.dispatch([=, &context] + { + const auto listenCount = parseListenCount(msgBody); + if (listenCount) + LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount; + + bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) }; + context.listenCount = listenCount; + + if (!needSync) + { + onSyncEnded(context); + return; + } + + context.maxDateTime = Wt::WDateTime::currentDateTime(); + enqueGetListens(context); + }); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void ListensSynchronizer::enqueGetListens(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/user/" + context.listenBrainzUserName + "/listens?max_ts=" + std::to_string(context.maxDateTime.toTime_t()); + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [=, &context](std::string_view msgBody) + { + processGetListensResponse(msgBody, context); + if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid()) + { + onSyncEnded(context); + return; + } + + enqueGetListens(context); + }; + request.onFailureFunc = [=, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context) + { + Database::Session& session{ _db.getTLSSession() }; + + context.maxDateTime = {}; // invalidate to break in case no more listens are fetched + ListensParser::Result result{ ListensParser::parse(msgBody) }; + context.fetchedListenCount += result.listenCount; + + for (const Listen& parsedListen : result.listens) + { + // update oldest listen for the next query + if (!parsedListen.listenedAt.isValid()) + { + LOG(DEBUG) << "Skipping entry due to invalid listenedAt"; + continue; + } + + if (!context.maxDateTime.isValid() || context.maxDateTime > parsedListen.listenedAt) + context.maxDateTime = parsedListen.listenedAt; + + if (const Database::TrackId trackId{ tryGetMatchingTrack(session, parsedListen) }; trackId.isValid()) + { + context.matchedListenCount++; + + const Scrobbling::TimedListen listen{ {context.userId, trackId}, parsedListen.listenedAt }; + if (saveListen(listen, Database::SyncState::Synchronized)) + context.importedListenCount++; + } + } + } } // namespace Scrobbling::ListenBrainz diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index f1634147..721577d0 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -204,16 +204,13 @@ namespace API::Subsonic if (!user) throw UserNotAuthorizedError{}; - const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; + const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& randomSongsNode{ response.createNode("randomSongs") }; - for (const TrackId trackId : trackIds.results) - { - const Track::pointer track{ Track::find(context.dbSession, trackId) }; + for (const Track::pointer& track : tracks.results) randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); - } return response; } @@ -250,12 +247,9 @@ namespace API::Subsonic params.setClusters({ cluster->getId() }); params.setRange({ offset, size }); - auto trackIds{ Track::find(context.dbSession, params) }; - for (const TrackId trackId : trackIds.results) - { - const Track::pointer track{ Track::find(context.dbSession, trackId) }; + const auto tracks{ Track::find(context.dbSession, params) }; + for (const Track::pointer& track : tracks.results) songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); - } return response; } diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 87b750dd..27abdb77 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -194,7 +194,7 @@ namespace API::Subsonic params.setRange({ 0, meanTrackCountPerArtist }); params.setSortMethod(TrackSortMethod::Random); - const auto artistTracks{ Track::find(context.dbSession, params) }; + const auto artistTracks{ Track::findIds(context.dbSession, params) }; tracks.insert(std::end(tracks), std::begin(artistTracks.results), std::end(artistTracks.results)); @@ -225,7 +225,7 @@ namespace API::Subsonic params.setRange({ 0, meanTrackCountPerRelease }); params.setSortMethod(TrackSortMethod::Random); - const auto releaseTracks{ Track::find(context.dbSession, params) }; + const auto releaseTracks{ Track::findIds(context.dbSession, params) }; tracks.insert(std::end(tracks), std::begin(releaseTracks.results), std::end(releaseTracks.results)); @@ -349,11 +349,8 @@ namespace API::Subsonic directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName())); const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) }; - for (const TrackId trackId : tracks.results) - { - const Track::pointer track{ Track::find(context.dbSession, trackId) }; + for (const Track::pointer& track : tracks.results) directoryNode.addArrayChild("child", createSongNode(context, track, user)); - } } else throw BadParameterGenericError{ "id" }; @@ -432,11 +429,8 @@ namespace API::Subsonic Response::Node albumNode{ createAlbumNode(context, release, user, true /* id3 */) }; const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; - for (const TrackId trackId : tracks.results) - { - const Track::pointer track{ Track::find(context.dbSession, trackId) }; + for (const Track::pointer& track : tracks.results) albumNode.addArrayChild("song", createSongNode(context, track, user)); - } response.addNode("album", std::move(albumNode)); diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index 5d609795..59c33fae 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -86,8 +86,7 @@ namespace API::Subsonic params.setKeywords(keywords); params.setRange({ albumOffset, albumCount }); - RangeResults releases{ Release::find(context.dbSession, params) }; - for (const Release::pointer& release : releases.results) + for (const Release::pointer& release : Release::find(context.dbSession, params).results) searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); } @@ -97,12 +96,8 @@ namespace API::Subsonic params.setKeywords(keywords); params.setRange({ songOffset, songCount }); - RangeResults trackIds{ Track::find(context.dbSession, params) }; - for (const TrackId trackId : trackIds.results) - { - const auto track{ Track::find(context.dbSession, trackId) }; + for (const Track::pointer& track : Track::find(context.dbSession, params).results) searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); - } } return response; diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index b2aab8d6..42150cfc 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -110,6 +110,7 @@ namespace API::Subsonic trackResponse.setAttribute("playCount", Listen::getCount(context.dbSession, user->getId(), user->getScrobblingBackend(), track->getId())); trackResponse.setAttribute("path", getTrackPath(track)); { + // TODO, store this in DB std::error_code ec; const auto fileSize{ std::filesystem::file_size(track->getPath(), ec) }; if (!ec) diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 94d5cf34..fab4cc70 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -51,788 +51,756 @@ #include "ModalManager.hpp" #include "Utils.hpp" -namespace UserInterface { - -namespace +namespace UserInterface { - class CreateTrackListModel : public Wt::WFormModel - { - public: - static inline const Field NameField {"name"}; - - CreateTrackListModel() - { - addField(NameField); - setValidator(NameField, createMandatoryValidator()); - } - - Wt::WString getName() const { return valueText(NameField); } - }; - - class ReplaceTrackListModel : public Wt::WFormModel - { - public: - static inline const Field NameField {"name"}; - using TrackListModel = ValueStringModel; - - ReplaceTrackListModel() - { - addField(NameField); - setValidator(NameField, createMandatoryValidator()); - } - - Database::TrackListId getTrackListId() const - { - auto row {trackListModel->getRowFromString(valueText(NameField))}; - return trackListModel->getValue(*row); - } - - static - std::shared_ptr - createTrackListModel() - { - using namespace Database; - - auto model {std::make_shared()}; - - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - TrackList::FindParameters params; - params.setType(TrackListType::Playlist); - params.setUser(LmsApp->getUserId()); - params.setSortMethod(TrackListSortMethod::Name); - - auto tracklists {TrackList::find(LmsApp->getDbSession(), params)}; - for (const TrackListId trackListId : tracklists.results) - { - const TrackList::pointer trackList {TrackList::find(LmsApp->getDbSession(), trackListId)}; - model->add(Wt::WString::fromUTF8(std::string {trackList->getName()}), trackListId); - } - - return model; - } - - std::shared_ptr trackListModel {createTrackListModel()}; - }; -} - -PlayQueue::PlayQueue() -: Template {Wt::WString::tr("Lms.PlayQueue.template")} -, _capacity {Service::get()->getULong("playqueue-max-entry-count", 1000)} -{ - initTrackLists(); - - addFunction("id", &Wt::WTemplate::Functions::id); - addFunction("tr", &Wt::WTemplate::Functions::tr); - - Wt::WPushButton* clearBtn {bindNew("clear-btn", Wt::WString::tr("Lms.PlayQueue.template.clear-btn"), Wt::TextFormat::XHTML)}; - clearBtn->clicked().connect([=] - { - clearTracks(); - }); - - Wt::WPushButton* saveBtn {bindNew("save-btn", Wt::WString::tr("Lms.PlayQueue.template.save-btn"), Wt::TextFormat::XHTML)}; - saveBtn->clicked().connect([=] - { - saveAsTrackList(); - }); - - _entriesContainer = bindNew("entries", Wt::WString::tr("Lms.PlayQueue.template.entry-container")); - _entriesContainer->onRequestElements.connect([this] - { - addSome(); - updateCurrentTrack(true); - }); - - Wt::WPushButton* shuffleBtn {bindNew("shuffle-btn", Wt::WString::tr("Lms.PlayQueue.template.shuffle-btn"), Wt::TextFormat::XHTML)}; - shuffleBtn->clicked().connect([=] - { - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - Database::TrackList::pointer queue {getQueue()}; - auto entries {queue->getEntries()}; - Random::shuffleContainer(entries); - - queue.modify()->clear(); - for (const Database::TrackListEntry::pointer& entry : entries) - LmsApp->getDbSession().create(entry->getTrack(), queue); - } - _entriesContainer->reset(); - addSome(); - }); - - _repeatBtn = bindNew("repeat-btn"); - _repeatBtn->clicked().connect([=] - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - if (!LmsApp->getUser()->isDemo()) - LmsApp->getUser().modify()->setRepeatAll(isRepeatAllSet()); - }); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (LmsApp->getUser()->isRepeatAllSet()) - _repeatBtn->setCheckState(Wt::CheckState::Checked); - } - - _radioBtn = bindNew("radio-btn"); - _radioBtn->clicked().connect([=] - { - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - if (!LmsApp->getUser()->isDemo()) - LmsApp->getUser().modify()->setRadio(isRadioModeSet()); - } - if (isRadioModeSet()) - enqueueRadioTracksIfNeeded(); - }); - - bool isRadioModeSet {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - isRadioModeSet = LmsApp->getUser()->isRadioSet(); - } - if (isRadioModeSet) - { - _radioBtn->setCheckState(Wt::CheckState::Checked); - enqueueRadioTracksIfNeeded(); - } - - _nbTracks = bindNew("track-count"); - _duration = bindNew("duration"); - - LmsApp->getMediaPlayer().settingsLoaded.connect([=] - { - if (_mediaPlayerSettingsLoaded) - return; - - _mediaPlayerSettingsLoaded = true; - - std::size_t trackPos {}; - - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - trackPos = LmsApp->getUser()->getCurPlayingTrackPos(); - } - - loadTrack(trackPos, false); - }); - - LmsApp->preQuit().connect([=] - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - if (LmsApp->getUser()->isDemo()) - { - LMS_LOG(UI, DEBUG) << "Removing queue (tracklist id " << _queueId.toString() << ")"; - if (Database::TrackList::pointer queue {getQueue()}) - queue.remove(); - } - }); - - updateInfo(); -} - -bool -PlayQueue::isRepeatAllSet() const -{ - return _repeatBtn->checkState() == Wt::CheckState::Checked; -} - -bool -PlayQueue::isRadioModeSet() const -{ - return _radioBtn->checkState() == Wt::CheckState::Checked; -} - -Database::TrackList::pointer -PlayQueue::getQueue() const -{ - return Database::TrackList::find(LmsApp->getDbSession(), _queueId); -} - -bool -PlayQueue::isFull() const -{ - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - return getQueue()->getCount() == getCapacity(); -} - -void -PlayQueue::clearTracks() -{ - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - getQueue().modify()->clear(); - } - - _entriesContainer->reset(); - _trackPos.reset(); - updateInfo(); -} - -void -PlayQueue::stop() -{ - updateCurrentTrack(false); - _trackPos.reset(); - _isTrackSelected = false; - trackUnselected.emit(); -} - -void -PlayQueue::loadTrack(std::size_t pos, bool play) -{ - updateCurrentTrack(false); - - Database::TrackId trackId {}; - std::optional replayGain {}; - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::TrackList::pointer queue {getQueue()}; - - // If out of range, stop playing - if (pos >= queue->getCount()) - { - if (!isRepeatAllSet() || queue->getCount() == 0) - { - stop(); - return; - } - - pos = 0; - } - - _trackPos = pos; - const Database::Track::pointer track {queue->getEntry(*_trackPos)->getTrack()}; - - trackId = track->getId(); - - replayGain = getReplayGain(pos, track); - - if (!LmsApp->getUser()->isDemo()) - LmsApp->getUser().modify()->setCurPlayingTrackPos(pos); - } - - enqueueRadioTracksIfNeeded(); - updateCurrentTrack(true); - _isTrackSelected = true; - trackSelected.emit(trackId, play, replayGain ? *replayGain : 0); -} - -void -PlayQueue::playPrevious() -{ - if (!_trackPos) - return; - - if (*_trackPos == 0) - stop(); - else - loadTrack(*_trackPos - 1, true); -} - -void -PlayQueue::playNext() -{ - if (!_trackPos) - { - loadTrack(0, true); - return; - } - - loadTrack(*_trackPos + 1, true); -} - -void -PlayQueue::onPlaybackEnded() -{ - playNext(); -} - -std::size_t -PlayQueue::getCount() -{ - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - return getQueue()->getCount(); -} - -void -PlayQueue::initTrackLists() -{ - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - Database::TrackList::pointer queue; - Database::TrackList::pointer radioStartingTracks; - - if (!LmsApp->getUser()->isDemo()) - { - static const std::string queueName {"__queued_tracks__"}; - queue = Database::TrackList::find(LmsApp->getDbSession(), queueName, Database::TrackListType::Internal, LmsApp->getUserId()); - if (!queue) - queue = LmsApp->getDbSession().create(queueName, Database::TrackListType::Internal, false, LmsApp->getUser()); - } - else - { - static const std::string queueName {"__temp_queue__"}; - queue = LmsApp->getDbSession().create(queueName, Database::TrackListType::Internal, false, LmsApp->getUser()); - } - - _queueId = queue->getId(); -} - -void -PlayQueue::updateInfo() -{ - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::TrackList::pointer queue {getQueue()}; - const std::size_t trackCount {queue->getCount()}; - _nbTracks->setText(Wt::WString::trn("Lms.track-count", trackCount).arg(trackCount)); - _duration->setText(Utils::durationToString(queue->getDuration())); - trackCountChanged.emit(trackCount); -} - -void -PlayQueue::updateCurrentTrack(bool selected) -{ - if (!_trackPos || *_trackPos >= static_cast(_entriesContainer->getCount())) - return; - - Template* entry {static_cast(_entriesContainer->getWidget(*_trackPos))}; - if (!entry) - return; - - entry->toggleStyleClass("Lms-entry-playing", selected); -} - -void -PlayQueue::enqueueTracks(const std::vector& trackIds) -{ - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - Database::TrackList::pointer queue {getQueue()}; - const std::size_t queueSize {queue->getCount()}; - - std::size_t nbTracksToEnqueue {queueSize + trackIds.size() > getCapacity() ? getCapacity() - queueSize : trackIds.size()}; - for (const Database::TrackId trackId : trackIds) - { - if (nbTracksToEnqueue == 0) - break; - - Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; - if (!track) - continue; - - LmsApp->getDbSession().create(track, queue); - nbTracksToEnqueue--; - } - } - - updateInfo(); - addSome(); - _entriesContainer->setHasMore(); -} - -std::vector -PlayQueue::getAndClearNextTracks() -{ - std::vector tracks; - - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - Database::TrackList::pointer queue {getQueue()}; - std::vector entries {queue->getEntries(Database::Range {_trackPos ? *_trackPos + 1 : 0, getCapacity()})}; - tracks.reserve(entries.size()); - for (Database::TrackListEntry::pointer entry : entries) - { - tracks.push_back(entry->getTrack()->getId()); - entry.remove(); - } - - if (_trackPos) - { - // entries may have been cleared - if (*_trackPos + 1 < _entriesContainer->getCount()) - _entriesContainer->remove(*_trackPos + 1, _entriesContainer->getCount() - 1); - } - else - { - _entriesContainer->reset(); - } - - return tracks; -} - -void -PlayQueue::play(const std::vector& trackIds) -{ - playAtIndex(trackIds, 0); -} - -void -PlayQueue::playNext(const std::vector& trackIds) -{ - std::vector nextTracks {getAndClearNextTracks()}; - nextTracks.insert(std::cbegin(nextTracks), std::cbegin(trackIds), std::cend(trackIds)); - playOrAddLast(nextTracks); -} - -void -PlayQueue::playShuffled(const std::vector& trackIds) -{ - clearTracks(); - std::vector shuffledTrackIds {trackIds}; - Random::shuffleContainer(shuffledTrackIds); - enqueueTracks(shuffledTrackIds); - loadTrack(0, true); -} - -void -PlayQueue::playOrAddLast(const std::vector& trackIds) -{ - enqueueTracks(trackIds); - if (!_isTrackSelected) - loadTrack(0, true); -} - -void -PlayQueue::playAtIndex(const std::vector& trackIds, std::size_t index) -{ - clearTracks(); - enqueueTracks(trackIds); - loadTrack(index, true); -} - -void -PlayQueue::addSome() -{ - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::TrackList::pointer queue {getQueue()}; - const auto tracklistEntries {queue->getEntries(Database::Range {_entriesContainer->getCount(), _batchSize})}; - for (const Database::TrackListEntry::pointer& tracklistEntry : tracklistEntries) - addEntry(tracklistEntry); -} - -void -PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) -{ - const Database::TrackListEntryId tracklistEntryId {tracklistEntry->getId()}; - const auto track {tracklistEntry->getTrack()}; - const Database::TrackId trackId {track->getId()}; - - Template* entry {_entriesContainer->addNew