From 4bc5aee98650102088ac4214fdc2dee0936fb6d2 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 7 Nov 2023 23:20:47 +0100 Subject: [PATCH 01/22] Subsonic API: added a small utility to bench search3 endpoint --- src/tools/subsonic-api-bench/search3.sh | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100755 src/tools/subsonic-api-bench/search3.sh diff --git a/src/tools/subsonic-api-bench/search3.sh b/src/tools/subsonic-api-bench/search3.sh new file mode 100755 index 00000000..897b2a01 --- /dev/null +++ b/src/tools/subsonic-api-bench/search3.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +if [ "$#" -ne 6 ]; then + echo "Usage: $0 " + exit 1 +fi + +# make any command failure exit +set -e + +read -s -p "Enter password: " user_password +echo + +# Retrieve the argument from the command line +base_url="$1" +user="$2" +artist_count="$3" +album_count="$4" +song_count="$5" +batch_size="$6" + +start_time=$(date +%s.%3N) + +# artists +for ((i = 0; i < artist_count; i += $batch_size)); do + wget -q -O - "$base_url/rest/search3.view?u=$user&p=$user_password&v=1.13.0&c=benchmark&f=json&query=&artistCount=$batch_size&artistOffset=$i&albumCount=0&songCount=0" > /dev/null +done + +# albums +for ((i = 0; i < album_count; i += $batch_size)); do + wget -q -O - "$base_url/rest/search3.view?u=$user&p=$user_password&v=1.13.0&c=benchmark&f=json&query=&artistCount=0&albumCount=$batch_size&albumOffset=$i&songCount=0" > /dev/null +done + +# songs +for ((i = 0; i < song_count; i += $batch_size)); do + wget -q -O - "$base_url/rest/search3.view?u=$user&p=$user_password&v=1.13.0&c=benchmark&f=json&query=&artistCount=0&albumCount=0&songCount=$batch_size&songOffset=$i" > /dev/null +done + +end_time=$(date +%s.%3N) + +elapsed_time=$(echo "$end_time - $start_time" | bc) + +echo "Fetch time: $elapsed_time seconds" \ No newline at end of file From 504dfa86a9d1d70f3d29b50b86f2afe626a88e0d Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 7 Nov 2023 23:51:04 +0100 Subject: [PATCH 02/22] Subsonic API: fixed missing mimetype for raw files --- src/libs/av/CMakeLists.txt | 1 + .../av/impl/RawResourceHandlerCreator.cpp | 38 +++++ .../include/av/RawResourceHandlerCreator.hpp | 30 ++++ .../impl/entrypoints/MediaRetrieval.cpp | 5 +- src/libs/utils/impl/FileResourceHandler.cpp | 152 +++++++++--------- src/libs/utils/impl/FileResourceHandler.hpp | 21 +-- .../utils/FileResourceHandlerCreator.hpp | 3 +- src/lms/ui/resource/AudioFileResource.cpp | 13 +- 8 files changed, 163 insertions(+), 100 deletions(-) create mode 100644 src/libs/av/impl/RawResourceHandlerCreator.cpp create mode 100644 src/libs/av/include/av/RawResourceHandlerCreator.hpp diff --git a/src/libs/av/CMakeLists.txt b/src/libs/av/CMakeLists.txt index 4205e35c..7c556cb3 100644 --- a/src/libs/av/CMakeLists.txt +++ b/src/libs/av/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(lmsav SHARED impl/AudioFile.cpp + impl/RawResourceHandlerCreator.cpp impl/Transcoder.cpp impl/TranscodeResourceHandler.cpp impl/Types.cpp diff --git a/src/libs/av/impl/RawResourceHandlerCreator.cpp b/src/libs/av/impl/RawResourceHandlerCreator.cpp new file mode 100644 index 00000000..5272b421 --- /dev/null +++ b/src/libs/av/impl/RawResourceHandlerCreator.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "av/RawResourceHandlerCreator.hpp" + +#include "av/IAudioFile.hpp" +#include "utils/FileResourceHandlerCreator.hpp" + +namespace Av +{ + std::unique_ptr createRawResourceHandler(const std::filesystem::path& path) + { + std::string mimeType; + const auto guessedAudioFormat{ Av::guessAudioFileFormat(path) }; + if (guessedAudioFormat) + mimeType = guessedAudioFormat->mimeType; + else + mimeType = "application/octet-stream"; + + return createFileResourceHandler(path, mimeType); + } +} diff --git a/src/libs/av/include/av/RawResourceHandlerCreator.hpp b/src/libs/av/include/av/RawResourceHandlerCreator.hpp new file mode 100644 index 00000000..ed7b0e14 --- /dev/null +++ b/src/libs/av/include/av/RawResourceHandlerCreator.hpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "utils/IResourceHandler.hpp" + +namespace Av +{ + std::unique_ptr createRawResourceHandler(const std::filesystem::path& path); +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index cddc758c..c452f48a 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -20,6 +20,7 @@ #include "MediaRetrieval.hpp" #include "av/IAudioFile.hpp" +#include "av/RawResourceHandlerCreator.hpp" #include "av/TranscodeParameters.hpp" #include "av/TranscodeResourceHandlerCreator.hpp" #include "av/Types.hpp" @@ -160,7 +161,7 @@ namespace API::Subsonic trackPath = track->getPath(); } - resourceHandler = createFileResourceHandler(trackPath); + resourceHandler = Av::createRawResourceHandler(trackPath); } else { @@ -185,7 +186,7 @@ namespace API::Subsonic if (streamParameters.transcodeParameters) resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); else - resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); + resourceHandler = Av::createRawResourceHandler(streamParameters.inputFileParameters.trackPath); } else { diff --git a/src/libs/utils/impl/FileResourceHandler.cpp b/src/libs/utils/impl/FileResourceHandler.cpp index 8e3a6eb0..b0b4e53f 100644 --- a/src/libs/utils/impl/FileResourceHandler.cpp +++ b/src/libs/utils/impl/FileResourceHandler.cpp @@ -24,108 +24,106 @@ #include "utils/Logger.hpp" std::unique_ptr -createFileResourceHandler(const std::filesystem::path& path) +createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) { - return std::make_unique(path); + return std::make_unique(path, mimeType); } - -FileResourceHandler::FileResourceHandler(const std::filesystem::path& path) -: _path {path} +FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType) + : _path{ path } + , _mimeType{ mimeType } { } - Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { - ::uint64_t startByte {_offset}; - std::ifstream ifs {_path.string().c_str(), std::ios::in | std::ios::binary}; + ::uint64_t startByte{ _offset }; + std::ifstream ifs{ _path.string().c_str(), std::ios::in | std::ios::binary }; - if (startByte == 0) - { - if (!ifs) - { - LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; - response.setStatus(404); - return {}; - } - else - { - response.setStatus(200); - } + if (startByte == 0) + { + if (!ifs) + { + LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; + response.setStatus(404); + return {}; + } - ifs.seekg(0, std::ios::end); - const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; - ifs.seekg(0, std::ios::beg); + ifs.seekg(0, std::ios::end); + const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) }; + ifs.seekg(0, std::ios::beg); - LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; + LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; - const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)}; - if (!ranges.isSatisfiable()) - { - std::ostringstream contentRange; - contentRange << "bytes */" << fileSize; - response.setStatus(416); // Requested range not satisfiable - response.addHeader("Content-Range", contentRange.str()); + const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) }; + if (!ranges.isSatisfiable()) + { + std::ostringstream contentRange; + contentRange << "bytes */" << fileSize; + response.setStatus(416); // Requested range not satisfiable + response.addHeader("Content-Range", contentRange.str()); - LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; - return {}; - } + LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; + return {}; + } - if (ranges.size() == 1) - { - LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); + if (ranges.size() == 1) + { + LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); - response.setStatus(206); - startByte = ranges[0].firstByte(); - _beyondLastByte = ranges[0].lastByte() + 1; + response.setStatus(206); + startByte = ranges[0].firstByte(); + _beyondLastByte = ranges[0].lastByte() + 1; - std::ostringstream contentRange; - contentRange << "bytes " << startByte << "-" - << _beyondLastByte - 1 << "/" << fileSize; + std::ostringstream contentRange; + contentRange << "bytes " << startByte << "-" + << _beyondLastByte - 1 << "/" << fileSize; - response.addHeader("Content-Range", contentRange.str()); - response.setContentLength(_beyondLastByte - startByte); - } - else - { - LMS_LOG(UTILS, DEBUG) << "No range requested"; + response.addHeader("Content-Range", contentRange.str()); + response.setContentLength(_beyondLastByte - startByte); + } + else + { + LMS_LOG(UTILS, DEBUG) << "No range requested"; - _beyondLastByte = fileSize; - response.setContentLength(_beyondLastByte); - } - } - else if (!ifs) - { - LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'"; - return {}; - } + response.setStatus(200); + _beyondLastByte = fileSize; + response.setContentLength(_beyondLastByte); + } - ifs.seekg(static_cast(startByte)); + LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'"; + response.setMimeType(_mimeType); + } + else if (!ifs) + { + LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'"; + return {}; + } - std::vector buf; - buf.resize(_chunkSize); + ifs.seekg(static_cast(startByte)); - ::uint64_t restSize = _beyondLastByte - startByte; - ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); + std::vector buf; + buf.resize(_chunkSize); - ifs.read(&buf[0], pieceSize); - const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())}; - response.out().write(&buf[0], actualPieceSize); + ::uint64_t restSize = _beyondLastByte - startByte; + ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); - LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; + ifs.read(&buf[0], pieceSize); + const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) }; + response.out().write(&buf[0], actualPieceSize); - LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; - if (ifs.good() && actualPieceSize < restSize) - { - _offset = startByte + actualPieceSize; - LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; + LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; - return response.createContinuation(); - } + LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; + if (ifs.good() && actualPieceSize < restSize) + { + _offset = startByte + actualPieceSize; + LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset; - LMS_LOG(UTILS, DEBUG) << "Job complete!"; - return nullptr; + return response.createContinuation(); + } + + LMS_LOG(UTILS, DEBUG) << "Job complete!"; + return nullptr; } - diff --git a/src/libs/utils/impl/FileResourceHandler.hpp b/src/libs/utils/impl/FileResourceHandler.hpp index 8293f587..c6e8e58a 100644 --- a/src/libs/utils/impl/FileResourceHandler.hpp +++ b/src/libs/utils/impl/FileResourceHandler.hpp @@ -20,21 +20,24 @@ #pragma once #include +#include +#include #include "utils/IResourceHandler.hpp" class FileResourceHandler final : public IResourceHandler { - public: - FileResourceHandler(const std::filesystem::path& filePath); +public: + FileResourceHandler(const std::filesystem::path& filePath, std::string_view mimeType); - private: - Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - void abort() override {}; +private: + Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + void abort() override {}; - static constexpr std::size_t _chunkSize {65536}; + static constexpr std::size_t _chunkSize{ 65536 }; - std::filesystem::path _path; - ::uint64_t _beyondLastByte {}; - ::uint64_t _offset {}; + std::filesystem::path _path; + std::string _mimeType; + ::uint64_t _beyondLastByte{}; + ::uint64_t _offset{}; }; diff --git a/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp index 8f69e18a..00106721 100644 --- a/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp +++ b/src/libs/utils/include/utils/FileResourceHandlerCreator.hpp @@ -21,8 +21,9 @@ #include #include +#include #include "utils/IResourceHandler.hpp" -std::unique_ptr createFileResourceHandler(const std::filesystem::path& path); +std::unique_ptr createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType); diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index e7a8809b..293e7aac 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -23,9 +23,9 @@ #include #include "av/IAudioFile.hpp" +#include "av/RawResourceHandlerCreator.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" -#include "utils/FileResourceHandlerCreator.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" #include "LmsApplication.hpp" @@ -94,16 +94,7 @@ AudioFileResource::handleRequest(const Wt::Http::Request& request, if (!trackPath) return; - fileResourceHandler = createFileResourceHandler(*trackPath); - - const auto guessedAudioFormat {Av::guessAudioFileFormat(*trackPath)}; - if (guessedAudioFormat) - { - LOG(DEBUG) << "Set mime type to " << guessedAudioFormat->mimeType; - response.setMimeType(guessedAudioFormat->mimeType); - } - else - response.setMimeType("application/octet-stream"); + fileResourceHandler = Av::createRawResourceHandler(*trackPath); } else { From 6bb6dd6add7b2e55189df821ef4aa7f2a3da00fd Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 13:53:27 +0100 Subject: [PATCH 03/22] Added some foreachSubRange --- .../include/services/database/Types.hpp | 365 +++++++++--------- src/libs/services/database/test/Common.cpp | 278 +++++++------ 2 files changed, 349 insertions(+), 294 deletions(-) diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index a07ec274..881d3f97 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -26,208 +26,225 @@ namespace Database { - // Caution: do not change enum values if they are set! + // Caution: do not change enum values if they are set! - // Request: - // size = 0 => no size limit! - // Response (via RangeResults) - // size => results size - struct Range - { - std::size_t offset {}; - std::size_t size {}; + // Request: + // size = 0 => no size limit! + // Response (via RangeResults) + // size => results size + struct Range + { + std::size_t offset{}; + std::size_t size{}; - // TODO remove this - operator bool() const { return size != 0; } - }; + // TODO remove this + operator bool() const { return size != 0; } + }; - template - struct RangeResults - { - Range range; - std::vector results; - bool moreResults{}; + // Func must return true to continue iterating + template + void foreachSubRange(Range range, std::size_t subRangeSize, Func&& func) + { + assert(subRangeSize > 0); - RangeResults getSubRange(Range subRange) - { - assert(subRange.offset >= range.offset); + Range subRange{ range.offset, std::min(range.size, subRangeSize) }; + while (subRange.size > 0) + { + if (!func(subRange)) + break; - if (!subRange.size) - subRange.size = range.size - (subRange.offset - range.offset); + subRange.offset += subRange.size; + subRange.size = std::min(subRangeSize, range.size - (subRange.offset - range.offset)); + } + } - subRange.offset = std::min(subRange.offset, range.offset + range.size); - subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); + template + struct RangeResults + { + Range range; + std::vector results; + bool moreResults{}; - RangeResults subResults; + RangeResults getSubRange(Range subRange) + { + assert(subRange.offset >= range.offset); - auto itBegin {std::cbegin(results) + subRange.offset - range.offset}; - auto itEnd {itBegin + subRange.size}; - subResults.results.reserve(std::distance(itBegin, itEnd)); - std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); + if (!subRange.size) + subRange.size = range.size - (subRange.offset - range.offset); - subResults.range = subRange; - if (subRange.offset + subRange.size == range.offset + range.size) - subResults.moreResults = moreResults; - else - subResults.moreResults = true; + subRange.offset = std::min(subRange.offset, range.offset + range.size); + subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); - return subResults; - } - }; + RangeResults subResults; - struct DateRange - { - Wt::WDate begin; - Wt::WDate end; + auto itBegin{ std::cbegin(results) + subRange.offset - range.offset }; + auto itEnd{ itBegin + subRange.size }; + subResults.results.reserve(std::distance(itBegin, itEnd)); + std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); - static DateRange fromYearRange(int from, int to); - }; + subResults.range = subRange; + if (subRange.offset + subRange.size == range.offset + range.size) + subResults.moreResults = moreResults; + else + subResults.moreResults = true; - struct DiscInfo - { - std::size_t position; - std::string name; - }; + return subResults; + } + }; - enum class ArtistSortMethod - { - None, - ByName, - BySortName, - Random, - LastWritten, - StarredDateDesc, - }; + struct DateRange + { + Wt::WDate begin; + Wt::WDate end; - enum class ReleaseSortMethod - { - None, - Name, - Date, - OriginalDate, - OriginalDateDesc, - Random, - LastWritten, - StarredDateDesc, - }; + static DateRange fromYearRange(int from, int to); + }; - enum class TrackListSortMethod - { - None, - Name, - LastModifiedDesc, - }; + struct DiscInfo + { + std::size_t position; + std::string name; + }; - enum class TrackSortMethod - { - None, - Random, - LastWritten, - StarredDateDesc, - Name, - DateDescAndRelease, - Release, // order by disc/track number - TrackList, // order by asc order in tracklist - }; + enum class ArtistSortMethod + { + None, + ByName, + BySortName, + Random, + LastWritten, + StarredDateDesc, + }; - enum class TrackArtistLinkType - { - Artist = 0, // regular track artist - Arranger = 1, - Composer = 2, - Conductor = 3, - Lyricist = 4, - Mixer = 5, - Performer = 6, - Producer = 7, - ReleaseArtist = 8, - Remixer = 9, - Writer = 10, - }; + enum class ReleaseSortMethod + { + None, + Name, + Date, + OriginalDate, + OriginalDateDesc, + Random, + LastWritten, + StarredDateDesc, + }; - // User selectable audio file formats - enum class AudioFormat - { - MP3 = 1, - OGG_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, - MATROSKA_OPUS = 5, - }; + enum class TrackListSortMethod + { + None, + Name, + LastModifiedDesc, + }; - using Bitrate = std::uint32_t; - // Do not remove values! - void visitAllowedAudioBitrates(std::function); - bool isAudioBitrateAllowed(Bitrate bitrate); + enum class TrackSortMethod + { + None, + Random, + LastWritten, + StarredDateDesc, + Name, + DateDescAndRelease, + Release, // order by disc/track number + TrackList, // order by asc order in tracklist + }; - enum class ScrobblingBackend - { - Internal = 0, - ListenBrainz = 1, - }; + enum class TrackArtistLinkType + { + Artist = 0, // regular track artist + Arranger = 1, + Composer = 2, + Conductor = 3, + Lyricist = 4, + Mixer = 5, + Performer = 6, + Producer = 7, + ReleaseArtist = 8, + Remixer = 9, + Writer = 10, + }; - enum class FeedbackBackend - { - Internal = 0, - ListenBrainz = 1, - }; + // User selectable audio file formats + enum class AudioFormat + { + MP3 = 1, + OGG_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, + MATROSKA_OPUS = 5, + }; - enum class SyncState - { - PendingAdd = 0, - Synchronized = 1, - PendingRemove = 2, - }; + using Bitrate = std::uint32_t; + // Do not remove values! + void visitAllowedAudioBitrates(std::function); + bool isAudioBitrateAllowed(Bitrate bitrate); - enum class UserType - { - REGULAR = 0, - ADMIN = 1, - DEMO = 2, - }; + enum class ScrobblingBackend + { + Internal = 0, + ListenBrainz = 1, + }; - enum class UITheme - { - Light = 0, - Dark = 1, - }; + enum class FeedbackBackend + { + Internal = 0, + ListenBrainz = 1, + }; - enum class SubsonicArtistListMode - { - AllArtists = 0, - ReleaseArtists = 1, - TrackArtists = 2, - }; + enum class SyncState + { + PendingAdd = 0, + Synchronized = 1, + PendingRemove = 2, + }; - enum class TrackListType - { - Playlist, // user controlled playlists - Internal, // internal usage (current playqueue, history, ...) - }; + enum class UserType + { + REGULAR = 0, + ADMIN = 1, + DEMO = 2, + }; - // as defined in https://musicbrainz.org/doc/Release_Group/Type - enum class ReleaseTypePrimary - { - Album, - Single, - EP, - Broadcast, - Other, - }; + enum class UITheme + { + Light = 0, + Dark = 1, + }; - enum class ReleaseTypeSecondary - { - Compilation, - Soundtrack, - Spokenword, - Interview, - Audiobook, - AudioDrama, - Live, - Remix, - DJMix, - Mixtape_Street, - Demo, - }; + enum class SubsonicArtistListMode + { + AllArtists = 0, + ReleaseArtists = 1, + TrackArtists = 2, + }; + + enum class TrackListType + { + Playlist, // user controlled playlists + Internal, // internal usage (current playqueue, history, ...) + }; + + // as defined in https://musicbrainz.org/doc/Release_Group/Type + enum class ReleaseTypePrimary + { + Album, + Single, + EP, + Broadcast, + Other, + }; + + enum class ReleaseTypeSecondary + { + Compilation, + Soundtrack, + Spokenword, + Interview, + Audiobook, + AudioDrama, + Live, + Remix, + DJMix, + Mixtape_Street, + Demo, + }; } diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp index c32d8932..68c49382 100644 --- a/src/libs/services/database/test/Common.cpp +++ b/src/libs/services/database/test/Common.cpp @@ -36,174 +36,212 @@ #include "services/database/User.hpp" TmpDatabase::TmpDatabase() -: _tmpFile {std::tmpnam(nullptr)} -, _fileDeleter {_tmpFile} -, _db {_tmpFile} + : _tmpFile{ std::tmpnam(nullptr) } + , _fileDeleter{ _tmpFile } + , _db{ _tmpFile } { } Database::Db& TmpDatabase::getDb() { - return _db; + return _db; } DatabaseFixture::~DatabaseFixture() { - testDatabaseEmpty(); + testDatabaseEmpty(); } void DatabaseFixture::SetUpTestCase() { - _tmpDb = std::make_unique(); - { - Database::Session s {_tmpDb->getDb()}; - s.prepareTables(); - s.analyze(); + _tmpDb = std::make_unique(); + { + Database::Session s{ _tmpDb->getDb() }; + s.prepareTables(); + s.analyze(); - // remove default created entries - { - auto transaction {s.createUniqueTransaction()}; - auto clusterTypes {Database::ClusterType::find(s, Database::Range {})}; - for (auto clusterTypeId : clusterTypes.results) - { - auto clusterType {Database::ClusterType::find(s, clusterTypeId)}; - clusterType.remove(); - } - } - } + // remove default created entries + { + auto transaction{ s.createUniqueTransaction() }; + auto clusterTypes{ Database::ClusterType::find(s, Database::Range {}) }; + for (auto clusterTypeId : clusterTypes.results) + { + auto clusterType{ Database::ClusterType::find(s, clusterTypeId) }; + clusterType.remove(); + } + } + } } void DatabaseFixture::TearDownTestCase() { - _tmpDb.reset(); + _tmpDb.reset(); } void DatabaseFixture::testDatabaseEmpty() { - using namespace Database; + using namespace Database; - auto uniqueTransaction {session.createUniqueTransaction()}; + auto uniqueTransaction{ session.createUniqueTransaction() }; - EXPECT_EQ(Artist::getCount(session), 0); - EXPECT_EQ(Cluster::getCount(session), 0); - EXPECT_EQ(ClusterType::getCount(session), 0); - EXPECT_EQ(Listen::getCount(session), 0); - EXPECT_EQ(Release::getCount(session), 0); - EXPECT_EQ(StarredArtist::getCount(session), 0); - EXPECT_EQ(StarredRelease::getCount(session), 0); - EXPECT_EQ(StarredTrack::getCount(session), 0); - EXPECT_EQ(Track::getCount(session), 0); - EXPECT_EQ(TrackBookmark::getCount(session), 0); - EXPECT_EQ(TrackList::getCount(session), 0); - EXPECT_EQ(User::getCount(session), 0); + EXPECT_EQ(Artist::getCount(session), 0); + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + EXPECT_EQ(Listen::getCount(session), 0); + EXPECT_EQ(Release::getCount(session), 0); + EXPECT_EQ(StarredArtist::getCount(session), 0); + EXPECT_EQ(StarredRelease::getCount(session), 0); + EXPECT_EQ(StarredTrack::getCount(session), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_EQ(TrackBookmark::getCount(session), 0); + EXPECT_EQ(TrackList::getCount(session), 0); + EXPECT_EQ(User::getCount(session), 0); } TEST_F(DatabaseFixture, Common_subRangeEmpty) { - using namespace Database; + using namespace Database; - RangeResults results; - results.range = Range {0, 0}; - results.results = {}; - results.moreResults = false; + RangeResults results; + results.range = Range{ 0, 0 }; + results.results = {}; + results.moreResults = false; - { - auto subRange {results.getSubRange(Range {0, 0})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - EXPECT_EQ(subRange.range, Range {}); - } - { - auto subRange {results.getSubRange(Range {0, 1})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - } + { + auto subRange{ results.getSubRange(Range {0, 0}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + EXPECT_EQ(subRange.range, Range{}); + } + { + auto subRange{ results.getSubRange(Range {0, 1}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Common_subRangeForeach) +{ + using namespace Database; + + struct TestCase + { + Range range; + std::size_t subRangeSize; + std::vector expectedSubRanges; + }; + + TestCase testCases[] + { + {Range{0, 0}, 1, {}}, + {Range{1, 0}, 1, {}}, + {Range{1, 1}, 1, { Range{ 1,1 } }}, + {Range{1, 3}, 1, { Range{ 1,1 }, Range {2,1}, Range{3,1} }}, + {Range{0, 100}, 100, { Range{0,100} }}, + {Range{0, 50}, 100, { Range{0,50} }}, + {Range{100, 200}, 100, { Range{100,100}, Range{200,100} }}, + {Range{100, 101}, 100, { Range{100,100}, Range{200,1}}}, + {Range{1000, 10}, 100, { Range{1000,10} }}, + {Range{1, 100}, 50, { Range{1,50}, Range{51, 50} }}, + }; + + for (const TestCase& test : testCases) + { + std::vector subRanges; + foreachSubRange(test.range, test.subRangeSize, [&](Range subRange) + { + subRanges.push_back(subRange); + return true; + }); + + EXPECT_EQ(subRanges, test.expectedSubRanges) << ", test index = " << std::distance(std::cbegin(testCases), &test); + } } TEST_F(DatabaseFixture, Common_IdType) { - using namespace Database; + using namespace Database; - { - const IdType id{}; - EXPECT_FALSE(id.isValid()); - } + { + const IdType id{}; + EXPECT_FALSE(id.isValid()); + } - { - const IdType id{0}; - EXPECT_TRUE(id.isValid()); - } + { + const IdType id{ 0 }; + EXPECT_TRUE(id.isValid()); + } - { - const IdType id1{0}; - const IdType id2{0}; - EXPECT_EQ(id1, id2); - } + { + const IdType id1{ 0 }; + const IdType id2{ 0 }; + EXPECT_EQ(id1, id2); + } - { - const IdType id1{0}; - const IdType id2{1}; - EXPECT_NE(id1, id2); - EXPECT_LT(id1, id2); - EXPECT_GT(id2, id1); - } + { + const IdType id1{ 0 }; + const IdType id2{ 1 }; + EXPECT_NE(id1, id2); + EXPECT_LT(id1, id2); + EXPECT_GT(id2, id1); + } } TEST_F(DatabaseFixture, Common_subRange) { - using namespace Database; + using namespace Database; - RangeResults results; - results.range = Range {0, 2}; - results.results = {5, 6}; - results.moreResults = false; + RangeResults results; + results.range = Range{ 0, 2 }; + results.results = { 5, 6 }; + results.moreResults = false; - { - auto subRange {results.getSubRange(Range {0, 1})}; - EXPECT_TRUE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 5); - } - { - auto subRange {results.getSubRange(Range {1, 1})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 6); - } - { - auto subRange {results.getSubRange(Range {0, 2})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 2); - EXPECT_EQ(subRange.results.front(), 5); - EXPECT_EQ(subRange.results.back(), 6); - } - { - auto subRange {results.getSubRange(Range {})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 2); - EXPECT_EQ(subRange.results.front(), 5); - EXPECT_EQ(subRange.results.back(), 6); - EXPECT_EQ(subRange.range, results.range); - } + { + auto subRange{ results.getSubRange(Range {0, 1}) }; + EXPECT_TRUE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 5); + } + { + auto subRange{ results.getSubRange(Range {1, 1}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + } + { + auto subRange{ results.getSubRange(Range {0, 2}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + } + { + auto subRange{ results.getSubRange(Range {}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + EXPECT_EQ(subRange.range, results.range); + } - { - auto subRange {results.getSubRange(Range {1, 0})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 1); - EXPECT_EQ(subRange.results.front(), 6); - const Range expectedRange {1, 1}; - EXPECT_EQ(subRange.range, expectedRange); - } - { - auto subRange {results.getSubRange(Range {3, 2})}; - EXPECT_FALSE(subRange.moreResults); - ASSERT_EQ(subRange.results.size(), 0); - const Range expectedRange {2, 0}; - EXPECT_EQ(subRange.range, expectedRange); - } + { + auto subRange{ results.getSubRange(Range {1, 0}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + const Range expectedRange{ 1, 1 }; + EXPECT_EQ(subRange.range, expectedRange); + } + { + auto subRange{ results.getSubRange(Range {3, 2}) }; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + const Range expectedRange{ 2, 0 }; + EXPECT_EQ(subRange.range, expectedRange); + } } From c2e1c5d9dfd39a8c5e565fd3f273d7c24100bc94 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 20:16:31 +0100 Subject: [PATCH 04/22] Fixed bad optimize condition --- .../scanner/impl/ScanStepScanFiles.cpp | 836 +++++++++--------- 1 file changed, 418 insertions(+), 418 deletions(-) diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index 86820bd5..77157b6b 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -37,500 +37,500 @@ using namespace Database; namespace { - Artist::pointer - createArtist(Session& session, const MetaData::Artist& artistInfo) - { - Artist::pointer artist {session.create(artistInfo.name)}; + Artist::pointer + createArtist(Session& session, const MetaData::Artist& artistInfo) + { + Artist::pointer artist{ session.create(artistInfo.name) }; - if (artistInfo.mbid) - artist.modify()->setMBID(*artistInfo.mbid); - if (artistInfo.sortName) - artist.modify()->setSortName(*artistInfo.sortName); + if (artistInfo.mbid) + artist.modify()->setMBID(*artistInfo.mbid); + if (artistInfo.sortName) + artist.modify()->setSortName(*artistInfo.sortName); - return artist; - } + return artist; + } - void - updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo) - { - // Name may have been updated - if (artist->getName() != artistInfo.name) - { - artist.modify()->setName(artistInfo.name); - } + void + updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo) + { + // Name may have been updated + if (artist->getName() != artistInfo.name) + { + artist.modify()->setName(artistInfo.name); + } - // Sortname may have been updated - if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) - { - artist.modify()->setSortName(*artistInfo.sortName); - } - } + // Sortname may have been updated + if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName()) + { + artist.modify()->setSortName(*artistInfo.sortName); + } + } - std::vector - getOrCreateArtists(Session& session, const std::vector& artistsInfo, bool allowFallbackOnMBIDEntries) - { - std::vector artists; + std::vector + getOrCreateArtists(Session& session, const std::vector& artistsInfo, bool allowFallbackOnMBIDEntries) + { + std::vector artists; - for (const MetaData::Artist& artistInfo : artistsInfo) - { - Artist::pointer artist; + for (const MetaData::Artist& artistInfo : artistsInfo) + { + Artist::pointer artist; - // First try to get by MBID - if (artistInfo.mbid) - { - artist = Artist::find(session, *artistInfo.mbid); - if (!artist) - artist = createArtist(session, artistInfo); - else - updateArtistIfNeeded(artist, artistInfo); + // First try to get by MBID + if (artistInfo.mbid) + { + artist = Artist::find(session, *artistInfo.mbid); + if (!artist) + artist = createArtist(session, artistInfo); + else + updateArtistIfNeeded(artist, artistInfo); - artists.emplace_back(std::move(artist)); - continue; - } + artists.emplace_back(std::move(artist)); + continue; + } - // Fall back on artist name (collisions may occur) - if (!artistInfo.name.empty()) - { - for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) - { - // Do not fallback on artist that is correctly tagged - if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) - continue; + // Fall back on artist name (collisions may occur) + if (!artistInfo.name.empty()) + { + for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) + { + // Do not fallback on artist that is correctly tagged + if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) + continue; - artist = sameNamedArtist; - break; - } + artist = sameNamedArtist; + break; + } - // No Artist found with the same name and without MBID -> creating - if (!artist) - artist = createArtist(session, artistInfo); - else - updateArtistIfNeeded(artist, artistInfo); + // No Artist found with the same name and without MBID -> creating + if (!artist) + artist = createArtist(session, artistInfo); + else + updateArtistIfNeeded(artist, artistInfo); - artists.emplace_back(std::move(artist)); - continue; - } - } + artists.emplace_back(std::move(artist)); + continue; + } + } - return artists; - } + return artists; + } - ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type) - { - switch (type) - { - case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album; - case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single; - case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP; - case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast; - case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other; - } + ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type) + { + switch (type) + { + case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album; + case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single; + case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP; + case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast; + case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other; + } - return ReleaseTypePrimary::Other; - } + return ReleaseTypePrimary::Other; + } - EnumSet convertReleaseTypesSecondary(EnumSet types) - { - EnumSet res; + EnumSet convertReleaseTypesSecondary(EnumSet types) + { + EnumSet res; - for (MetaData::Release::SecondaryType type : types) - { - switch (type) - { - case MetaData::Release::SecondaryType::Compilation: - res.insert(ReleaseTypeSecondary::Compilation); - break; - case MetaData::Release::SecondaryType::Soundtrack: - res.insert(ReleaseTypeSecondary::Soundtrack); - break; - case MetaData::Release::SecondaryType::Spokenword: - res.insert(ReleaseTypeSecondary::Spokenword); - break; - case MetaData::Release::SecondaryType::Interview: - res.insert(ReleaseTypeSecondary::Interview); - break; - case MetaData::Release::SecondaryType::Audiobook: - res.insert(ReleaseTypeSecondary::Audiobook); - break; - case MetaData::Release::SecondaryType::AudioDrama: - res.insert(ReleaseTypeSecondary::AudioDrama); - break; - case MetaData::Release::SecondaryType::Live: - res.insert(ReleaseTypeSecondary::Live); - break; - case MetaData::Release::SecondaryType::Remix: - res.insert(ReleaseTypeSecondary::Remix); - break; - case MetaData::Release::SecondaryType::DJMix: - res.insert(ReleaseTypeSecondary::DJMix); - break; - case MetaData::Release::SecondaryType::Mixtape_Street: - res.insert(ReleaseTypeSecondary::Mixtape_Street); - break; - case MetaData::Release::SecondaryType::Demo: - res.insert(ReleaseTypeSecondary::Demo); - break; - } - } + for (MetaData::Release::SecondaryType type : types) + { + switch (type) + { + case MetaData::Release::SecondaryType::Compilation: + res.insert(ReleaseTypeSecondary::Compilation); + break; + case MetaData::Release::SecondaryType::Soundtrack: + res.insert(ReleaseTypeSecondary::Soundtrack); + break; + case MetaData::Release::SecondaryType::Spokenword: + res.insert(ReleaseTypeSecondary::Spokenword); + break; + case MetaData::Release::SecondaryType::Interview: + res.insert(ReleaseTypeSecondary::Interview); + break; + case MetaData::Release::SecondaryType::Audiobook: + res.insert(ReleaseTypeSecondary::Audiobook); + break; + case MetaData::Release::SecondaryType::AudioDrama: + res.insert(ReleaseTypeSecondary::AudioDrama); + break; + case MetaData::Release::SecondaryType::Live: + res.insert(ReleaseTypeSecondary::Live); + break; + case MetaData::Release::SecondaryType::Remix: + res.insert(ReleaseTypeSecondary::Remix); + break; + case MetaData::Release::SecondaryType::DJMix: + res.insert(ReleaseTypeSecondary::DJMix); + break; + case MetaData::Release::SecondaryType::Mixtape_Street: + res.insert(ReleaseTypeSecondary::Mixtape_Street); + break; + case MetaData::Release::SecondaryType::Demo: + res.insert(ReleaseTypeSecondary::Demo); + break; + } + } - return res; - } + return res; + } - void - updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo) - { - if (release->getName() != releaseInfo.name) - release.modify()->setName(releaseInfo.name); - if (release->getTotalDisc() != releaseInfo.mediumCount) - release.modify()->setTotalDisc(releaseInfo.mediumCount); - if (releaseInfo.primaryType) - { - const ReleaseTypePrimary primaryType {convertReleaseTypePrimary(*releaseInfo.primaryType)}; - if (release->getPrimaryType() != primaryType) - release.modify()->setPrimaryType(primaryType); - } - const EnumSet secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; - if (release->getSecondaryTypes() != secondaryTypes) - release.modify()->setSecondaryTypes(secondaryTypes); - if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) - release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); - } + void + updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo) + { + if (release->getName() != releaseInfo.name) + release.modify()->setName(releaseInfo.name); + if (release->getTotalDisc() != releaseInfo.mediumCount) + release.modify()->setTotalDisc(releaseInfo.mediumCount); + if (releaseInfo.primaryType) + { + const ReleaseTypePrimary primaryType{ convertReleaseTypePrimary(*releaseInfo.primaryType) }; + if (release->getPrimaryType() != primaryType) + release.modify()->setPrimaryType(primaryType); + } + const EnumSet secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; + if (release->getSecondaryTypes() != secondaryTypes) + release.modify()->setSecondaryTypes(secondaryTypes); + if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) + release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); + } - Release::pointer - getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo) - { - Release::pointer release; + Release::pointer + getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo) + { + Release::pointer release; - // First try to get by MBID - if (releaseInfo.mbid) - { - release = Release::find(session, *releaseInfo.mbid); - if (!release) - release = session.create(releaseInfo.name, releaseInfo.mbid); + // First try to get by MBID + if (releaseInfo.mbid) + { + release = Release::find(session, *releaseInfo.mbid); + if (!release) + release = session.create(releaseInfo.name, releaseInfo.mbid); - updateReleaseIfNeeded(release, releaseInfo); - return release; - } + updateReleaseIfNeeded(release, releaseInfo); + return release; + } - // Fall back on release name (collisions may occur) - if (!releaseInfo.name.empty()) - { - for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name)) - { - // do not fallback on properly tagged releases - if (sameNamedRelease->getMBID()) - continue; + // Fall back on release name (collisions may occur) + if (!releaseInfo.name.empty()) + { + for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name)) + { + // do not fallback on properly tagged releases + if (sameNamedRelease->getMBID()) + continue; - release = sameNamedRelease; - break; - } + release = sameNamedRelease; + break; + } - // No release found with the same name and without MBID -> creating - if (!release) - release = session.create(releaseInfo.name); + // No release found with the same name and without MBID -> creating + if (!release) + release = session.create(releaseInfo.name); - updateReleaseIfNeeded(release, releaseInfo); - return release; - } + updateReleaseIfNeeded(release, releaseInfo); + return release; + } - return Release::pointer{}; - } + return Release::pointer{}; + } - std::vector - getOrCreateClusters(Session& session, const MetaData::Tags& tags) - { - std::vector clusters; + std::vector + getOrCreateClusters(Session& session, const MetaData::Tags& tags) + { + std::vector clusters; - for (const auto& [tag, values] : tags) - { - auto clusterType = ClusterType::find(session, tag); - if (!clusterType) - continue; + for (const auto& [tag, values] : tags) + { + auto clusterType = ClusterType::find(session, tag); + if (!clusterType) + continue; - for (auto clusterName : values) - { - auto cluster = clusterType->getCluster(clusterName); - if (!cluster) - cluster = session.create(clusterType, clusterName); + for (auto clusterName : values) + { + auto cluster = clusterType->getCluster(clusterName); + if (!cluster) + cluster = session.create(clusterType, clusterName); - clusters.push_back(cluster); - } - } + clusters.push_back(cluster); + } + } - return clusters; - } + return clusters; + } - MetaData::ParserReadStyle - getParserReadStyle() - { - std::string_view readStyle {Service::get()->getString("scanner-parser-read-style", "accurate")}; + MetaData::ParserReadStyle + getParserReadStyle() + { + std::string_view readStyle{ Service::get()->getString("scanner-parser-read-style", "accurate") }; - if (readStyle == "fast") - return MetaData::ParserReadStyle::Fast; - else if (readStyle == "average") - return MetaData::ParserReadStyle::Average; - else if (readStyle == "accurate") - return MetaData::ParserReadStyle::Accurate; + if (readStyle == "fast") + return MetaData::ParserReadStyle::Fast; + else if (readStyle == "average") + return MetaData::ParserReadStyle::Average; + else if (readStyle == "accurate") + return MetaData::ParserReadStyle::Accurate; - throw LmsException {"Invalid value for 'scanner-parser-read-style'"}; - } + throw LmsException{ "Invalid value for 'scanner-parser-read-style'" }; + } } // namespace namespace Scanner { - ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) - : ScanStepBase {initParams} - , _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib - { - } + ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) + : ScanStepBase{ initParams } + , _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib + { + } - void - ScanStepScanFiles::process(ScanContext& context) - { - _metadataParser->setClusterTypeNames(_settings.clusterTypeNames); + void + ScanStepScanFiles::process(ScanContext& context) + { + _metadataParser->setClusterTypeNames(_settings.clusterTypeNames); - context.currentStepStats.totalElems = context.stats.filesScanned; + context.currentStepStats.totalElems = context.stats.filesScanned; - PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path) - { - if (_abortScan) - return false; + PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path) + { + if (_abortScan) + return false; - if (ec) - { - LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); - context.stats.errors.emplace_back(ScanError {path, ScanErrorType::CannotReadFile, ec.message()}); - } - else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) - { - scanAudioFile(path, context); + if (ec) + { + LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); + context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() }); + } + else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) + { + scanAudioFile(path, context); - context.currentStepStats.processedElems++; - _progressCallback(context.currentStepStats); + context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); - // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) - if ((context.stats.nbChanges() % 5'000) == 0) - _db.getTLSSession().optimize(); - } + // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) + if ((context.currentStepStats.processedElems % 1'000) == 0) + _db.getTLSSession().optimize(); + } - return true; - }, &excludeDirFileName); - } + return true; + }, &excludeDirFileName); + } - void - ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context) - { - ScanStats& stats {context.stats}; - Wt::WDateTime lastWriteTime; - try - { - lastWriteTime = PathUtils::getLastWriteTime(file); - } - catch (LmsException& e) - { - LMS_LOG(DBUPDATER, ERROR) << e.what(); - stats.skips++; - return; - } + void + ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context) + { + ScanStats& stats{ context.stats }; + Wt::WDateTime lastWriteTime; + try + { + lastWriteTime = PathUtils::getLastWriteTime(file); + } + catch (LmsException& e) + { + LMS_LOG(DBUPDATER, ERROR) << e.what(); + stats.skips++; + return; + } - if (!context.forceScan) - { - // Skip file if last write is the same - Database::Session& dbSession {_db.getTLSSession()}; - auto transaction {_db.getTLSSession().createSharedTransaction()}; + if (!context.forceScan) + { + // Skip file if last write is the same + Database::Session& dbSession{ _db.getTLSSession() }; + auto transaction{ _db.getTLSSession().createSharedTransaction() }; - const Track::pointer track {Track::findByPath(dbSession, file)}; + const Track::pointer track{ Track::findByPath(dbSession, file) }; - if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() - && track->getScanVersion() == _settings.scanVersion) - { - stats.skips++; - return; - } - } + if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() + && track->getScanVersion() == _settings.scanVersion) + { + stats.skips++; + return; + } + } - std::optional trackInfo {_metadataParser->parse(file)}; - if (!trackInfo) - { - context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); - return; - } + std::optional trackInfo{ _metadataParser->parse(file) }; + if (!trackInfo) + { + context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); + return; + } - stats.scans++; + stats.scans++; - Database::Session& dbSession {_db.getTLSSession()}; - auto uniqueTransaction {dbSession.createUniqueTransaction()}; + Database::Session& dbSession{ _db.getTLSSession() }; + auto uniqueTransaction{ dbSession.createUniqueTransaction() }; - Track::pointer track {Track::findByPath(dbSession, file) }; + Track::pointer track{ Track::findByPath(dbSession, file) }; - if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID)) - { - std::vector duplicateTracks {Track::findByMBID(dbSession, *trackInfo->mbid)}; + if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID)) + { + std::vector duplicateTracks{ Track::findByMBID(dbSession, *trackInfo->mbid) }; - // find for existing MBIDs as the file may have just been moved - if (!track && duplicateTracks.size() == 1) - { - Track::pointer otherTrack {duplicateTracks.front()}; - std::error_code ec; - if (!std::filesystem::exists(otherTrack->getPath(), ec)) - { - LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"; - track = otherTrack; - track.modify()->setPath(file); - } - } + // find for existing MBIDs as the file may have just been moved + if (!track && duplicateTracks.size() == 1) + { + Track::pointer otherTrack{ duplicateTracks.front() }; + std::error_code ec; + if (!std::filesystem::exists(otherTrack->getPath(), ec)) + { + LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"; + track = otherTrack; + track.modify()->setPath(file); + } + } - // Skip duplicate track MBID - if (_settings.skipDuplicateMBID) - { - for (Track::pointer otherTrack : duplicateTracks) - { - // Skip ourselves - if (track && track->getId() == otherTrack->getId()) - continue; + // Skip duplicate track MBID + if (_settings.skipDuplicateMBID) + { + for (Track::pointer otherTrack : duplicateTracks) + { + // Skip ourselves + if (track && track->getId() == otherTrack->getId()) + continue; - // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file - if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName)) - continue; + // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file + if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName)) + continue; - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"; - // As this MBID already exists, just remove what we just scanned - if (track) - { - track.remove(); - stats.deletions++; - } - return; - } - } - } + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"; + // As this MBID already exists, just remove what we just scanned + if (track) + { + track.remove(); + stats.deletions++; + } + return; + } + } + } - // We estimate this is an audio file if: - // - we found a least one audio stream - // - the duration is not null - if (trackInfo->audioStreams.empty()) - { - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)"; + // We estimate this is an audio file if: + // - we found a least one audio stream + // - the duration is not null + if (trackInfo->audioStreams.empty()) + { + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)"; - // If Track exists here, delete it! - if (track) - { - track.remove(); - stats.deletions++; - } - stats.errors.emplace_back(ScanError {file, ScanErrorType::NoAudioTrack}); - return; - } - if (trackInfo->duration == std::chrono::milliseconds::zero()) - { - LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; + // If Track exists here, delete it! + if (track) + { + track.remove(); + stats.deletions++; + } + stats.errors.emplace_back(ScanError{ file, ScanErrorType::NoAudioTrack }); + return; + } + if (trackInfo->duration == std::chrono::milliseconds::zero()) + { + LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)"; - // If Track exists here, delete it! - if (track) - { - track.remove(); - stats.deletions++; - } - stats.errors.emplace_back(ScanError {file, ScanErrorType::BadDuration}); - return; - } + // If Track exists here, delete it! + if (track) + { + track.remove(); + stats.deletions++; + } + stats.errors.emplace_back(ScanError{ file, ScanErrorType::BadDuration }); + return; + } - // ***** Title - std::string title; - if (!trackInfo->title.empty()) - title = trackInfo->title; - else - { - // TODO parse file name guess track etc. - // For now juste use file name as title - title = file.filename().string(); - } + // ***** Title + std::string title; + if (!trackInfo->title.empty()) + title = trackInfo->title; + else + { + // TODO parse file name guess track etc. + // For now juste use file name as title + title = file.filename().string(); + } - // If file already exists, update its data - // Otherwise, create it - if (!track) - { - track = dbSession.create(file); - LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'"; - stats.additions++; - } - else - { - LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'"; + // If file already exists, update its data + // Otherwise, create it + if (!track) + { + track = dbSession.create(file); + LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'"; + stats.additions++; + } + else + { + LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'"; - stats.updates++; - } + stats.updates++; + } - // Track related data - assert(track); + // Track related data + assert(track); - track.modify()->clearArtistLinks(); - // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files - for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist)); + track.modify()->clearArtistLinks(); + // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files + for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist)); - if (trackInfo->medium && trackInfo->medium->release) - { - for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); - } + if (trackInfo->medium && trackInfo->medium->release) + { + for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); + } - // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists - // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process - for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor)); + // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists + // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process + for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor)); - for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer)); + for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer)); - for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); + for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); - for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer)); + for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer)); - for (const auto& [role, performers] : trackInfo->performerArtists) - { - for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role)); - } + for (const auto& [role, performers] : trackInfo->performerArtists) + { + for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role)); + } - for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer)); + for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer)); - for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true)) - track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer)); + for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true)) + track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer)); - track.modify()->setScanVersion(_settings.scanVersion); - if (trackInfo->medium && trackInfo->medium->release) - track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release)); - else - track.modify()->setRelease({}); - track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt); - track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt); - track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : ""); - track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags)); - track.modify()->setLastWriteTime(lastWriteTime); - track.modify()->setName(title); - track.modify()->setDuration(trackInfo->duration); - track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); - track.modify()->setTrackNumber(trackInfo->position); - track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); - track.modify()->setDate(trackInfo->date); - track.modify()->setOriginalDate(trackInfo->originalDate); + track.modify()->setScanVersion(_settings.scanVersion); + if (trackInfo->medium && trackInfo->medium->release) + track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release)); + else + track.modify()->setRelease({}); + track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt); + track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt); + track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : ""); + track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags)); + track.modify()->setLastWriteTime(lastWriteTime); + track.modify()->setName(title); + track.modify()->setDuration(trackInfo->duration); + track.modify()->setAddedTime(Wt::WDateTime::currentDateTime()); + track.modify()->setTrackNumber(trackInfo->position); + track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); + track.modify()->setDate(trackInfo->date); + track.modify()->setOriginalDate(trackInfo->originalDate); - // If a file has an OriginalYear but no Year, set it to ease filtering - if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) - track.modify()->setDate(trackInfo->originalDate); + // If a file has an OriginalYear but no Year, set it to ease filtering + if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) + track.modify()->setDate(trackInfo->originalDate); - track.modify()->setRecordingMBID(trackInfo->recordingMBID); - track.modify()->setTrackMBID(trackInfo->mbid); - if (auto trackFeatures {TrackFeatures::find(dbSession, track->getId())}) - trackFeatures.remove(); // TODO: only if MBID changed? - track.modify()->setHasCover(trackInfo->hasCover); - track.modify()->setCopyright(trackInfo->copyright); - track.modify()->setCopyrightURL(trackInfo->copyrightURL); - track.modify()->setTrackReplayGain(trackInfo->replayGain); - track.modify()->setArtistDisplayName(trackInfo->artistDisplayName); - } + track.modify()->setRecordingMBID(trackInfo->recordingMBID); + track.modify()->setTrackMBID(trackInfo->mbid); + if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) }) + trackFeatures.remove(); // TODO: only if MBID changed? + track.modify()->setHasCover(trackInfo->hasCover); + track.modify()->setCopyright(trackInfo->copyright); + track.modify()->setCopyrightURL(trackInfo->copyrightURL); + track.modify()->setTrackReplayGain(trackInfo->replayGain); + track.modify()->setArtistDisplayName(trackInfo->artistDisplayName); + } } From 121d27b5cd115de4be4d58bafeacb6b7f075d138 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 20:26:24 +0100 Subject: [PATCH 05/22] Subsonic API: cached cluster stats for track and releases, in order to make the getGenre entrypoint efficient on large databases --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + approot/messages_it.xml | 1 + approot/messages_zh.xml | 1 + src/libs/services/database/impl/Cluster.cpp | 57 +++++++-- src/libs/services/database/impl/Migration.cpp | 11 ++ src/libs/services/database/impl/Migration.hpp | 2 +- .../include/services/database/Cluster.hpp | 29 +++-- src/libs/services/database/test/Cluster.cpp | 34 +++-- src/libs/services/scanner/CMakeLists.txt | 1 + .../impl/ScanStepComputeClusterStats.cpp | 86 +++++++++++++ .../impl/ScanStepComputeClusterStats.hpp | 36 ++++++ .../services/scanner/impl/ScannerService.cpp | 2 + .../include/services/scanner/ScannerStats.hpp | 119 +++++++++--------- src/libs/subsonic/impl/responses/Album.cpp | 4 +- src/libs/subsonic/impl/responses/Song.cpp | 4 +- src/lms/ui/Utils.cpp | 2 +- src/lms/ui/admin/ScannerController.cpp | 5 + src/lms/ui/explore/Filters.cpp | 4 +- .../recommendation/LmsRecommendation.cpp | 2 +- 20 files changed, 301 insertions(+), 101 deletions(-) create mode 100644 src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp create mode 100644 src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp diff --git a/approot/messages.xml b/approot/messages.xml index cdaba83b..e895b0ef 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -85,6 +85,7 @@ Scanning: step {1}/{2} Checking for duplicate files... {1} files Checking files... {1}% +Computing stats... {1}% Discovering files: {1} files Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)... Reloading similarity engine: {1}%... diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index c67ab668..59152411 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -85,6 +85,7 @@ En cours de scan : étape {1}/{2} Vérification des fichiers dupliqués... {1} fichiers Vérification des fichiers... {1}% +Calcul des statistiques... {1}% Découverte des fichiers : {1} fichiers Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)... Rechargement du moteur de recommandation : {1}%... diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 518fc9b6..0a5f358a 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -85,6 +85,7 @@ Scansione: passo {1}/{2} Controllo file... {1}% + File trovati: {1} files Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)... Ricarica motore di tracce simili: {1}%... diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 9442202c..7eb6472f 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -85,6 +85,7 @@ 扫描中: 阶段 {1}/{2} 检查文件中... {1}% + 检索文件中: {1} 文件 从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)... 重载相似引擎中 {1}%... diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp index 1dcbf1b7..675d1691 100644 --- a/src/libs/services/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -32,11 +32,12 @@ namespace Database { namespace { - Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params) + template + Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const Cluster::FindParameters& params) { session.checkSharedLocked(); - auto query{ session.getDboSession().query("SELECT DISTINCT c.id,c.name FROM cluster c") }; + auto query{ session.getDboSession().query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; if (params.track.isValid() || params.release.isValid()) { @@ -54,6 +55,21 @@ namespace Database return query; } + + template + Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params) + { + std::string_view itemToSelect; + + if constexpr (std::is_same_v) + itemToSelect = "c.id"; + else if constexpr (std::is_same_v>) + itemToSelect = "c"; + else + static_assert("Unhandled type"); + + return createQuery(session, itemToSelect, params); + } } Cluster::Cluster(ObjectPtr type, std::string_view name) @@ -74,10 +90,18 @@ namespace Database return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); } - RangeResults Cluster::find(Session& session, const FindParameters& params) + RangeResults Cluster::findIds(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query{ createQuery(session, params) }; + auto query{ createQuery(session, params) }; + + return Utils::execQuery(query, params.range); + } + + RangeResults Cluster::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); + auto query{ createQuery>(session, params) }; return Utils::execQuery(query, params.range); } @@ -97,6 +121,22 @@ namespace Database return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } + std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id).resultValue(); + } + + std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("t_c.cluster_id = ?").bind(id).resultValue(); + } + void Cluster::addTrack(ObjectPtr track) { _tracks.insert(getDboPtr(track)); @@ -112,15 +152,6 @@ namespace Database return Utils::execQuery(query, range); } - std::size_t Cluster::getReleasesCount() const - { - assert(session()); - - return session()->query("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("c.id = ?").bind(getId()); - } - - ClusterType::ClusterType(std::string_view name) : _name{ name } { diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index da002ba0..06639393 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -224,6 +224,16 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend"); } + static void migrateFromV43(Session& session) + { + // add counts in genre table + session.getDboSession().execute("ALTER TABLE cluster ADD track_count INTEGER"); + session.getDboSession().execute("ALTER TABLE cluster ADD release_count INTEGER"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(session).modify()->incScanVersion(); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -245,6 +255,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {40, migrateFromV40}, {41, migrateFromV41}, {42, migrateFromV42}, + {43, migrateFromV43}, }; { diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 55b1e679..1c671b51 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION{ 43 }; + static constexpr Version LMS_DATABASE_VERSION{ 44 }; class VersionInfo { public: diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp index 521727e6..fa7c1c0a 100644 --- a/src/libs/services/database/include/services/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -59,26 +59,35 @@ namespace Database { Cluster() = default; // Find utility - // As clusters only have a name, this is an optim to directly get the cluster names - using ClusterFindResult = std::tuple; static std::size_t getCount(Session& session); - static RangeResults find(Session& session, const FindParameters& range); + static RangeResults findIds(Session& session, const FindParameters& range); + static RangeResults find(Session& session, const FindParameters& range); + static void find(Session& session, const FindParameters& range, std::function _func); static pointer find(Session& session, ClusterId id); static RangeResults findOrphans(Session& session, Range range); - // Accessors - const std::string& getName() const { return _name; } - ObjectPtr getType() const { return _clusterType; } - std::size_t getTracksCount() const { return _tracks.size(); } - RangeResults getTracks(Range range) const; - std::size_t getReleasesCount() const; + // May be very slow + static std::size_t computeTrackCount(Session& session, ClusterId id); + static std::size_t computeReleaseCount(Session& session, ClusterId id); + // Accessors + std::string_view getName() const { return _name; } + ObjectPtr getType() const { return _clusterType; } + std::size_t getTracksCount() const { return _trackCount; } + RangeResults getTracks(Range range) const; + std::size_t getReleasesCount() const { return _releaseCount; }; + + void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; } + void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; } void addTrack(ObjectPtr track); template void persist(Action& a) { Wt::Dbo::field(a, _name, "name"); + // cached field since queries are too long + Wt::Dbo::field(a, _trackCount, "track_count"); + Wt::Dbo::field(a, _releaseCount, "release_count"); Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); @@ -92,6 +101,8 @@ namespace Database { static const std::size_t _maxNameLength = 128; std::string _name; + int _trackCount{}; + int _releaseCount{}; Wt::Dbo::ptr _clusterType; Wt::Dbo::collection< Wt::Dbo::ptr > _tracks; diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index 8e0186b6..075fdb9a 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -49,9 +49,9 @@ TEST_F(DatabaseFixture, Cluster) EXPECT_EQ(cluster->getType()->getId(), clusterType.getId()); { - const auto clusters{ Cluster::find(session, Cluster::FindParameters {}) }; + const auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}) }; ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster.getId()); + EXPECT_EQ(clusters.results.front(), cluster.getId()); } { @@ -106,6 +106,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) EXPECT_EQ(clusters.results.size(), 2); EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusterIds().empty()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { @@ -116,9 +118,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) }; + auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}.setTrack(track.getId())) }; ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster1.getId()); + EXPECT_EQ(clusters.results.front(), cluster1.getId()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { @@ -182,6 +186,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; EXPECT_TRUE(tracks.results.empty()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } { @@ -196,6 +202,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; ASSERT_FALSE(tracks.results.empty()); EXPECT_EQ(tracks.results.front(), track.getId()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1); } } @@ -219,7 +227,7 @@ TEST_F(DatabaseFixture, Cluster_multiTracks) auto transaction{ session.createSharedTransaction() }; EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_EQ(cluster->getTracksCount(), tracks.size()); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size()); for (TrackId trackId : cluster->getTracks(Range{}).results) { @@ -344,6 +352,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) ASSERT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty()); EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); } { @@ -361,14 +371,16 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); } + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); } { auto transaction{ session.createSharedTransaction() }; - const auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) }; + const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) }; ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(std::get(clusters.results.front()), cluster.getId()); + EXPECT_EQ(clusters.results.front(), cluster.getId()); } { @@ -389,10 +401,10 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_EQ(cluster->getReleasesCount(), 1); - EXPECT_EQ(cluster->getTracksCount(), 1); - EXPECT_EQ(unusedCluster->getReleasesCount(), 0); - EXPECT_EQ(unusedCluster->getTracksCount(), 0); + EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), 1); + EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); + EXPECT_EQ(Cluster::computeTrackCount(session, unusedCluster.getId()), 0); } } diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index e913996f..8a0508e9 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(lmsscanner SHARED impl/ScannerService.cpp impl/ScannerStats.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp + impl/ScanStepComputeClusterStats.cpp impl/ScanStepDiscoverFiles.cpp impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepScanFiles.cpp diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp new file mode 100644 index 00000000..38399599 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ScanStepComputeClusterStats.hpp" +#include "services/database/Db.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Session.hpp" +#include "utils/Logger.hpp" +#include "utils/Path.hpp" + +namespace Scanner +{ + void ScanStepComputeClusterStats::process(ScanContext& context) + { + using namespace Database; + + if (context.stats.nbChanges() == 0) + return; + + Session& dbSession{ _db.getTLSSession() }; + + const std::size_t clusterCount{ [&] { + auto transaction{ dbSession.createSharedTransaction() }; + return Cluster::getCount(dbSession); + }() }; + + context.currentStepStats.totalElems = clusterCount; + + foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range) + { + const std::vector clusterIds{ [&] + { + Cluster::FindParameters params; + params.setRange(range); + + { + auto transaction{ dbSession.createSharedTransaction() }; + return std::move(Cluster::findIds(dbSession, params).results); + } + }() }; + + for (const ClusterId clusterId : clusterIds) + { + std::size_t trackCount; + std::size_t releaseCount; + + { + auto transaction{ dbSession.createSharedTransaction() }; + + trackCount = Cluster::computeTrackCount(dbSession, clusterId); + releaseCount = Cluster::computeReleaseCount(dbSession, clusterId); + } + + { + auto transaction{ dbSession.createUniqueTransaction() }; + + auto cluster{ Cluster::find(dbSession, clusterId) }; + cluster.modify()->setTrackCount(trackCount); + cluster.modify()->setReleaseCount(releaseCount); + } + + context.currentStepStats.processedElems++; + } + + return true; + }); + + LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << clusterCount << " clusters!"; + } +} diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp new file mode 100644 index 00000000..4ccd460d --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "ScanStepBase.hpp" + +namespace Scanner +{ + class ScanStepComputeClusterStats : public ScanStepBase + { + public: + using ScanStepBase::ScanStepBase; + + private: + ScanStep getStep() const override { return ScanStep::ComputeClusterStats; } + std::string_view getStepName() const override { return "Compute cluster stats"; } + void process(ScanContext& context) override; + }; +} diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 3fa41ed2..81907181 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -36,6 +36,7 @@ #include "ScanStepDiscoverFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepScanFiles.hpp" +#include "ScanStepComputeClusterStats.hpp" using namespace Database; @@ -357,6 +358,7 @@ ScannerService::refreshScanSettings() _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); } diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index c0a8bba8..5801269f 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -28,80 +28,81 @@ namespace Scanner { - enum class ScanErrorType - { - CannotReadFile, // cannot read file - CannotParseFile, // cannot parse file - NoAudioTrack, // no audio track found - BadDuration, // bad duration - }; + enum class ScanErrorType + { + CannotReadFile, // cannot read file + CannotParseFile, // cannot parse file + NoAudioTrack, // no audio track found + BadDuration, // bad duration + }; - enum class DuplicateReason - { - SameHash, - SameTrackMBID, - }; + enum class DuplicateReason + { + SameHash, + SameTrackMBID, + }; - struct ScanError - { - std::filesystem::path file; - ScanErrorType error; - std::string systemError; + struct ScanError + { + std::filesystem::path file; + ScanErrorType error; + std::string systemError; - ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); - }; + ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); + }; - struct ScanDuplicate - { - Database::TrackId trackId; - DuplicateReason reason; - }; + struct ScanDuplicate + { + Database::TrackId trackId; + DuplicateReason reason; + }; - enum class ScanStep - { - DiscoveringFiles, - ScanningFiles, - ChekingForMissingFiles, - CheckingForDuplicateFiles, - FetchingTrackFeatures, - ReloadingSimilarityEngine, - }; - static inline constexpr unsigned ScanProgressStepCount {5}; + enum class ScanStep + { + DiscoveringFiles, + ScanningFiles, + ChekingForMissingFiles, + CheckingForDuplicateFiles, + FetchingTrackFeatures, + ReloadingSimilarityEngine, + ComputeClusterStats, + }; + static inline constexpr unsigned ScanProgressStepCount{ 6 }; - // reduced scan stats - struct ScanStepStats - { - Wt::WDateTime startTime; + // reduced scan stats + struct ScanStepStats + { + Wt::WDateTime startTime; - ScanStep currentStep; + ScanStep currentStep; - std::size_t totalElems {}; - std::size_t processedElems {}; + std::size_t totalElems{}; + std::size_t processedElems{}; - unsigned progress() const; - }; + unsigned progress() const; + }; - struct ScanStats - { - Wt::WDateTime startTime; - Wt::WDateTime stopTime; + struct ScanStats + { + Wt::WDateTime startTime; + Wt::WDateTime stopTime; - std::size_t filesScanned {}; // Total number of files scanned (estimated) + std::size_t filesScanned{}; // Total number of files scanned (estimated) - std::size_t skips {}; // no change since last scan - std::size_t scans {}; // actually scanned filed + std::size_t skips{}; // no change since last scan + std::size_t scans{}; // actually scanned filed - std::size_t additions {}; // added in DB - std::size_t deletions {}; // removed from DB - std::size_t updates {}; // updated file in DB + std::size_t additions{}; // added in DB + std::size_t deletions{}; // removed from DB + std::size_t updates{}; // updated file in DB - std::size_t featuresFetched {}; // features fetched in DB + std::size_t featuresFetched{}; // features fetched in DB - std::vector errors; - std::vector duplicates; + std::vector errors; + std::vector duplicates; - std::size_t nbFiles() const; - std::size_t nbChanges() const; - }; + std::size_t nbFiles() const; + std::size_t nbChanges() const; + }; } // namespace Scanner diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index cbee51d6..b9bff4f5 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -168,7 +168,7 @@ namespace API::Subsonic params.setClusterType(clusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayValue(field, std::get(cluster)); + albumNode.addArrayValue(field, cluster->getName()); } } }; @@ -183,7 +183,7 @@ namespace API::Subsonic params.setClusterType(genreClusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - albumNode.addArrayChild("genres", createItemGenreNode(std::get(cluster))); + albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName())); } albumNode.createEmptyArrayChild("artists"); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 42150cfc..ac86bfb9 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -228,7 +228,7 @@ namespace API::Subsonic params.setClusterType(clusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - trackResponse.addArrayValue(field, std::get(cluster)); + trackResponse.addArrayValue(field, cluster->getName()); } } }; @@ -243,7 +243,7 @@ namespace API::Subsonic params.setClusterType(genreClusterType->getId()); for (const auto& cluster : Cluster::find(context.dbSession, params).results) - trackResponse.addArrayChild("genres", createItemGenreNode(std::get(cluster))); + trackResponse.addArrayChild("genres", createItemGenreNode(cluster->getName())); } trackResponse.addChild("replayGain", createReplayGainNode(track)); diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 12c5fb93..c18f53de 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -107,7 +107,7 @@ namespace UserInterface::Utils } }; const std::string styleClass{ getStyleClass(cluster) }; - auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(cluster->getName()), Wt::TextFormat::UnsafeXHTML) }; + auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) }; res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK res->setToolTip(cluster->getType()->getName(), Wt::TextFormat::Plain); diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 4965eac4..cb4ea443 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -265,10 +265,15 @@ ScannerController::refreshContents() .arg(status.currentScanStepStats->totalElems) .arg(status.currentScanStepStats->progress())); break; + case Scanner::ScanStep::ReloadingSimilarityEngine: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-reloading-similarity-engine") .arg(status.currentScanStepStats->progress())); break; + + case Scanner::ScanStep::ComputeClusterStats: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") + .arg(status.currentScanStepStats->progress())); } break; } diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index 27d79f43..a3a08487 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -98,7 +98,7 @@ Filters::showDialog() for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); + valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); } } } @@ -115,7 +115,7 @@ Filters::showDialog() for (const Cluster::pointer& cluster : clusterType->getClusters()) { if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(cluster->getName())); + valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); } }); diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 33d73d6c..a4a71f17 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -61,7 +61,7 @@ static void dumpTracksRecommendation(Session session, Recommendation::IRecommend for (auto artist : track->getArtists({ TrackArtistLinkType::Artist })) res += " - " + artist->getName(); for (auto cluster : track->getClusters()) - res += " {" + cluster->getType()->getName() + "-" + cluster->getName() + "}"; + res += " {" + cluster->getType()->getName() + "-" + std::string{ cluster->getName() } + "}"; return res; }; From b89dfba0893181f66b535eace19b4ac027aa44de Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 22:47:41 +0100 Subject: [PATCH 06/22] Subsonic API: limit size parameter to 500 for various methods --- src/libs/subsonic/impl/SubsonicResponse.hpp | 12 ++++++++++++ .../subsonic/impl/entrypoints/AlbumSongLists.cpp | 12 ++++++++---- src/libs/subsonic/impl/entrypoints/Searching.cpp | 13 ++++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 4414d4d1..cf661a51 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -185,6 +185,18 @@ namespace API::Subsonic const std::string _parameterName; }; + class ParameterValueTooHighGenericError : public GenericError + { + public: + ParameterValueTooHighGenericError(std::string_view parameterName, std::size_t max) : _parameterName{ parameterName }, _max{ max } {} + + private: + std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; } + + const std::string _parameterName; + std::size_t _max; + }; + class Response { public: diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 721577d0..5dc3b2b4 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -47,6 +47,8 @@ namespace API::Subsonic // Optional params const std::size_t size{ getParameterAs(context.parameters, "size").value_or(10) }; const std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; + if (size > 500) + throw ParameterValueTooHighGenericError{ "size", 500 }; const Range range{ offset, size }; @@ -196,7 +198,8 @@ namespace API::Subsonic { // Optional params std::size_t size{ getParameterAs(context.parameters, "size").value_or(50) }; - size = std::min(size, std::size_t{ 500 }); + if (size > 500) + throw ParameterValueTooHighGenericError{"size", 500}; auto transaction{ context.dbSession.createSharedTransaction() }; @@ -221,8 +224,9 @@ namespace API::Subsonic std::string genre{ getMandatoryParameterAs(context.parameters, "genre") }; // Optional params - std::size_t size{ getParameterAs(context.parameters, "count").value_or(10) }; - size = std::min(size, std::size_t{ 500 }); + std::size_t count{ getParameterAs(context.parameters, "count").value_or(10) }; + if (count > 500) + throw ParameterValueTooHighGenericError{"count", 500}; std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; @@ -245,7 +249,7 @@ namespace API::Subsonic Track::FindParameters params; params.setClusters({ cluster->getId() }); - params.setRange({ offset, size }); + params.setRange({ offset, count }); const auto tracks{ Track::find(context.dbSession, params) }; for (const Track::pointer& track : tracks.results) diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index a3ed2968..79dccac7 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -57,15 +57,22 @@ namespace API::Subsonic std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; + if (artistCount > 500) + throw ParameterValueTooHighGenericError{ "artistCount", 500 }; + else if (albumCount > 500) + throw ParameterValueTooHighGenericError{ "albumCount", 500 }; + else if (songCount > 500) + throw ParameterValueTooHighGenericError{ "songCount", 500 }; + + Response response{ Response::createOkResponse(context.serverProtocolVersion) }; + Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; + auto transaction{ context.dbSession.createSharedTransaction() }; User::pointer user{ User::find(context.dbSession, context.userId) }; if (!user) throw UserNotAuthorizedError{}; - Response response{ Response::createOkResponse(context.serverProtocolVersion) }; - Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; - if (artistCount > 0) { Artist::FindParameters params; From 475519f19e931281ab68ca617a5dd7e2bc627dac Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 23:31:13 +0100 Subject: [PATCH 07/22] Subsonic API: improved error message --- src/libs/subsonic/impl/SubsonicResponse.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index cf661a51..18cef189 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -191,7 +191,7 @@ namespace API::Subsonic ParameterValueTooHighGenericError(std::string_view parameterName, std::size_t max) : _parameterName{ parameterName }, _max{ max } {} private: - std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; } + std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value (max is " + std::to_string(_max) + ")"; } const std::string _parameterName; std::size_t _max; From bd6109389852acea5ac653fcab6f9effb796a934 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 8 Nov 2023 23:43:50 +0100 Subject: [PATCH 08/22] Subsonic API: now returns 404 when the requested cover art is not found (but a client list has been added to serve them a default cover art) --- conf/lms.conf | 5 +- src/libs/services/cover/impl/CoverService.cpp | 6 - src/libs/services/cover/impl/CoverService.hpp | 134 ++++++++--------- .../include/services/cover/ICoverService.hpp | 26 ++-- src/libs/subsonic/impl/RequestContext.hpp | 1 + src/libs/subsonic/impl/SubsonicResource.cpp | 19 ++- src/libs/subsonic/impl/SubsonicResource.hpp | 2 + .../impl/entrypoints/MediaRetrieval.cpp | 18 ++- src/lms/ui/resource/CoverResource.cpp | 139 +++++++++--------- 9 files changed, 188 insertions(+), 162 deletions(-) diff --git a/conf/lms.conf b/conf/lms.conf index 07e7943f..02985108 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -61,7 +61,10 @@ api-subsonic = true; # Use this list to make the reported server version to 1.12.0 depending on the client's name # Main usage is to make auto detections for the 'p' (password) parameter work -api-subsonic-report-old-server-protocol = ("DSub"); +api-subsonic-old-server-protocol-clients = ("DSub"); + +# List of clients for whom a default cover is served (as they do not have their own) +api-subsonic-default-cover-clients = ("DSub", "substreamer"); # List of clients for whom open subsonic extensions and extra fields are disabled api-open-subsonic-disabled-clients = ("DSub"); diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 0912828e..91c8c612 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -345,9 +345,6 @@ namespace Cover } } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); @@ -397,9 +394,6 @@ namespace Cover cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); } - if (!cover) - cover = getDefault(width); - if (cover) saveToCache(cacheEntryDesc, cover); diff --git a/src/libs/services/cover/impl/CoverService.hpp b/src/libs/services/cover/impl/CoverService.hpp index f23d437a..dd5b82c5 100644 --- a/src/libs/services/cover/impl/CoverService.hpp +++ b/src/libs/services/cover/impl/CoverService.hpp @@ -35,103 +35,97 @@ namespace Database { - class Session; + class Session; } namespace Av { - class IAudioFile; + class IAudioFile; } namespace Cover { - struct CacheEntryDesc - { - std::variant id; - std::size_t size; - - bool operator==(const CacheEntryDesc& other) const - { - return id == other.id - && size == other.size; - } - }; + struct CacheEntryDesc + { + std::variant id; + std::size_t size; + bool operator==(const CacheEntryDesc& other) const + { + return id == other.id + && size == other.size; + } + }; } // ns Cover namespace std { - - template<> - class hash - { - public: - size_t operator()(const Cover::CacheEntryDesc& e) const - { - size_t h {}; - std::visit([&](auto id) - { - using IdType = std::decay_t; - h ^= std::hash()(id); - }, e.id); - h ^= std::hash()(e.size) << 1; - return h; - } - }; + template<> + class hash + { + public: + size_t operator()(const Cover::CacheEntryDesc& e) const + { + size_t h{}; + std::visit([&](auto id) + { + using IdType = std::decay_t; + h ^= std::hash()(id); + }, e.id); + h ^= std::hash()(e.size) << 1; + return h; + } + }; } // ns std namespace Cover { - class CoverService : public ICoverService - { - public: - CoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + class CoverService : public ICoverService + { + public: + CoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); - CoverService(const CoverService&) = delete; - CoverService& operator=(const CoverService&) = delete; - CoverService(CoverService&&) = delete; - CoverService& operator=(CoverService&&) = delete; + CoverService(const CoverService&) = delete; + CoverService& operator=(const CoverService&) = delete; - private: - std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; - std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; - void flushCache() override; - void setJpegQuality(unsigned quality) override; + private: + std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; + std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; + std::shared_ptr getDefault(Image::ImageSize width) override; + void flushCache() override; + void setJpegQuality(unsigned quality) override; - std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); - std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; - std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; + std::shared_ptr getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); + std::unique_ptr getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; + std::unique_ptr getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; - std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; - std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; - std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; - std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - std::shared_ptr getDefault(Image::ImageSize width); + std::unique_ptr getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; + std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; + std::unique_ptr getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; + std::unique_ptr getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; - bool checkCoverFile(const std::filesystem::path& directoryPath) const; + bool checkCoverFile(const std::filesystem::path& directoryPath) const; - Database::Db& _db; + Database::Db& _db; - std::shared_mutex _cacheMutex; - std::unordered_map> _cache; - std::unordered_map> _defaultCoverCache; - std::atomic _cacheMisses {}; - std::atomic _cacheHits {}; - std::size_t _cacheSize {}; + std::shared_mutex _cacheMutex; + std::unordered_map> _cache; + std::unordered_map> _defaultCoverCache; + std::atomic _cacheMisses{}; + std::atomic _cacheHits{}; + std::size_t _cacheSize{}; - void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); - std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); + void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr image); + std::shared_ptr loadFromCache(const CacheEntryDesc& entryDesc); - const std::filesystem::path _defaultCoverPath; - const std::size_t _maxCacheSize; - static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize - const std::size_t _maxFileSize; - const std::vector _preferredFileNames; - unsigned _jpegQuality; - }; + const std::filesystem::path _defaultCoverPath; + const std::size_t _maxCacheSize; + static inline const std::vector _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize + const std::size_t _maxFileSize; + const std::vector _preferredFileNames; + unsigned _jpegQuality; + }; } // namespace Cover diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 0421f589..7ce4a2df 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -28,27 +28,27 @@ namespace Database { - class Db; + class Db; } namespace Cover { - class ICoverService - { - public: - virtual ~ICoverService() = default; + class ICoverService + { + public: + virtual ~ICoverService() = default; - virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; - virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; + virtual std::shared_ptr getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; - virtual void flushCache() = 0; + virtual std::shared_ptr getDefault(Image::ImageSize width) = 0; - virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 - }; + virtual void flushCache() = 0; - std::unique_ptr createCoverService(Database::Db& db, - const std::filesystem::path& execPath, - const std::filesystem::path& defaultCoverPath); + virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 + }; + + std::unique_ptr createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); } // namespace CoverArt diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 05de5b5a..61b23f0a 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -42,6 +42,7 @@ namespace API::Subsonic ClientInfo clientInfo; ProtocolVersion serverProtocolVersion; bool enableOpenSubsonic{ true }; + bool enableDefaultCover{ }; }; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e5b0bb0d..4fc91b2a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -66,7 +66,7 @@ namespace API::Subsonic { std::unordered_map res; - Service::get()->visitStrings("api-subsonic-report-old-server-protocol", + Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", [&](std::string_view client) { res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 }); @@ -88,6 +88,19 @@ namespace API::Subsonic return res; } + std::unordered_set readDefaultCoverClients() + { + std::unordered_set res; + + Service::get()->visitStrings("api-subsonic-default-cover-clients", + [&](std::string_view client) + { + res.emplace(std::string{ client }); + }, { "DSub", "substreamer" }); + + return res; + } + std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { auto censorValue = [](const std::string& type, const std::string& value) -> std::string @@ -266,6 +279,7 @@ namespace API::Subsonic SubsonicResource::SubsonicResource(Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } + , _defaultCoverClients{ readDefaultCoverClients() } , _db{ db } { } @@ -378,8 +392,9 @@ namespace API::Subsonic const ClientInfo clientInfo{ getClientInfo(parameters) }; const Database::UserId userId{ authenticateUser(request, clientInfo) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; + bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) }; - return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic }; + return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover }; } Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index 63a04a5c..24c7025a 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -53,6 +53,8 @@ namespace API::Subsonic const std::unordered_map _serverProtocolVersionsByClient; const std::unordered_set _openSubsonicDisabledClients; + const std::unordered_set _defaultCoverClients; + Database::Db& _db; }; diff --git a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp index cddc758c..43729bcb 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaRetrieval.cpp @@ -207,8 +207,9 @@ namespace API::Subsonic // Mandatory params const auto trackId{ getParameterAs(context.parameters, "id") }; const auto releaseId{ getParameterAs(context.parameters, "id") }; + const auto artistId{ getParameterAs(context.parameters, "id") }; - if (!trackId && !releaseId) + if (!trackId && !releaseId && !artistId) throw BadParameterGenericError{ "id" }; std::size_t size{ getParameterAs(context.parameters, "size").value_or(1024) }; @@ -219,6 +220,21 @@ namespace API::Subsonic cover = Service::get()->getFromTrack(*trackId, size); else if (releaseId) cover = Service::get()->getFromRelease(*releaseId, size); + else if (artistId) + { + // TODO handle a placeholder for artists + response.setStatus(404); + return; + } + + if (!cover && context.enableDefaultCover) + cover = Service::get()->getDefault(size); + + if (!cover) + { + response.setStatus(404); + return; + } response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); response.setMimeType(std::string{ cover->getMimeType() }); diff --git a/src/lms/ui/resource/CoverResource.cpp b/src/lms/ui/resource/CoverResource.cpp index 6b4c5a42..c9cdd0ab 100644 --- a/src/lms/ui/resource/CoverResource.cpp +++ b/src/lms/ui/resource/CoverResource.cpp @@ -35,88 +35,89 @@ namespace UserInterface { -CoverResource::CoverResource() -{ - LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) - { - if (stats.nbChanges()) - setChanged(); - }); -} + CoverResource::CoverResource() + { + LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) + { + if (stats.nbChanges()) + setChanged(); + }); + } -CoverResource::~CoverResource() -{ - beingDeleted(); -} + CoverResource::~CoverResource() + { + beingDeleted(); + } -std::string -CoverResource::getReleaseUrl(Database::ReleaseId releaseId, Size size) const -{ - return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast(size)); -} + std::string CoverResource::getReleaseUrl(Database::ReleaseId releaseId, Size size) const + { + return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast(size)); + } -std::string -CoverResource::getTrackUrl(Database::TrackId trackId, Size size) const -{ - return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast(size)); -} + std::string CoverResource::getTrackUrl(Database::TrackId trackId, Size size) const + { + return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast(size)); + } -void -CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) -{ - // Retrieve parameters - const std::string *trackIdStr = request.getParameter("trackid"); - const std::string *releaseIdStr = request.getParameter("releaseid"); - const std::string *sizeStr = request.getParameter("size"); + void CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) + { + // Retrieve parameters + const std::string* trackIdStr = request.getParameter("trackid"); + const std::string* releaseIdStr = request.getParameter("releaseid"); + const std::string* sizeStr = request.getParameter("size"); - // Mandatory parameter size - if (!sizeStr) - { - LOG(DEBUG) << "no size provided!"; - return; - } + // Mandatory parameter size + if (!sizeStr) + { + LOG(DEBUG) << "no size provided!"; + return; + } - const auto size {StringUtils::readAs(*sizeStr)}; - if (!size || *size > maxSize) - { - LOG(DEBUG) << "invalid size provided!"; - return; - } + const auto size{ StringUtils::readAs(*sizeStr) }; + if (!size || *size > maxSize) + { + LOG(DEBUG) << "invalid size provided!"; + return; + } - std::shared_ptr cover; + std::shared_ptr cover; - if (trackIdStr) - { - LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; + if (trackIdStr) + { + LOG(DEBUG) << "Requested cover for track " << *trackIdStr << ", size = " << *size; - const std::optional trackId {StringUtils::readAs(*trackIdStr)}; - if (!trackId) - { - LOG(DEBUG) << "track not found"; - return; - } + const std::optional trackId{ StringUtils::readAs(*trackIdStr) }; + if (!trackId) + { + LOG(DEBUG) << "track not found"; + return; + } - cover = Service::get()->getFromTrack(*trackId, *size); - } - else if (releaseIdStr) - { - LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size; + cover = Service::get()->getFromTrack(*trackId, *size); + if (!cover) + cover = Service::get()->getDefault(*size); + } + else if (releaseIdStr) + { + LOG(DEBUG) << "Requested cover for release " << *releaseIdStr << ", size = " << *size; - const std::optional releaseId {StringUtils::readAs(*releaseIdStr)}; - if (!releaseId) - return; + const std::optional releaseId{ StringUtils::readAs(*releaseIdStr) }; + if (!releaseId) + return; - cover = Service::get()->getFromRelease(*releaseId, *size); - } - else - { - LOG(DEBUG) << "No track or release provided"; - return; - } + cover = Service::get()->getFromRelease(*releaseId, *size); + if (!cover) + cover = Service::get()->getDefault(*size); + } + else + { + LOG(DEBUG) << "No track or release provided"; + return; + } - response.setMimeType(std::string {cover->getMimeType()}); + response.setMimeType(std::string{ cover->getMimeType() }); - response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); -} + response.out().write(reinterpret_cast(cover->getData()), cover->getDataSize()); + } } // namespace UserInterface From b7a8bf85b8c376b4543298ae7206d0644589a30d Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 10 Nov 2023 16:17:18 +0100 Subject: [PATCH 09/22] Rework db range primitives + fixed bug when playing all in releases/tracks views + raised limit to 1000 for ranges in subsonic API --- src/libs/services/cover/impl/CoverService.cpp | 4 +- src/libs/services/database/impl/Artist.cpp | 21 +- src/libs/services/database/impl/Cluster.cpp | 24 +- src/libs/services/database/impl/Listen.cpp | 26 +- src/libs/services/database/impl/Release.cpp | 22 +- .../services/database/impl/StarredTrack.cpp | 2 +- src/libs/services/database/impl/Track.cpp | 35 +- .../database/impl/TrackArtistLink.cpp | 2 +- .../services/database/impl/TrackBookmark.cpp | 77 +- .../services/database/impl/TrackFeatures.cpp | 143 ++- src/libs/services/database/impl/TrackList.cpp | 952 ++++++------------ src/libs/services/database/impl/User.cpp | 2 +- src/libs/services/database/impl/Utils.hpp | 84 +- .../include/services/database/Artist.hpp | 9 +- .../include/services/database/Cluster.hpp | 29 +- .../include/services/database/Listen.hpp | 22 +- .../include/services/database/Release.hpp | 9 +- .../services/database/StarredTrack.hpp | 4 +- .../include/services/database/Track.hpp | 373 +++---- .../services/database/TrackArtistLink.hpp | 5 +- .../services/database/TrackBookmark.hpp | 3 +- .../services/database/TrackFeatures.hpp | 3 +- .../include/services/database/TrackList.hpp | 228 ++--- .../include/services/database/Types.hpp | 5 +- .../include/services/database/User.hpp | 4 +- src/libs/services/database/test/Artist.cpp | 18 +- src/libs/services/database/test/Cluster.cpp | 411 +------- src/libs/services/database/test/Common.cpp | 24 +- .../services/database/test/DatabaseTest.cpp | 4 +- src/libs/services/database/test/Release.cpp | 16 +- src/libs/services/database/test/Track.cpp | 16 + .../services/database/test/TrackBookmark.cpp | 2 +- .../services/database/test/TrackFeatures.cpp | 2 +- src/libs/services/database/test/TrackList.cpp | 577 +++-------- .../feedback/impl/FeedbackService.cpp | 50 +- .../feedback/impl/FeedbackService.hpp | 6 +- .../services/feedback/IFeedbackService.hpp | 37 +- .../impl/features/FeaturesEngine.cpp | 2 +- .../impl/ScanStepCheckDuplicatedDbFiles.cpp | 2 +- .../impl/ScanStepRemoveOrphanDbFiles.cpp | 271 +++-- src/libs/subsonic/impl/SubsonicResponse.hpp | 2 + .../impl/entrypoints/AlbumSongLists.cpp | 62 +- .../subsonic/impl/entrypoints/Bookmarks.cpp | 2 +- .../subsonic/impl/entrypoints/Browsing.cpp | 30 +- .../subsonic/impl/entrypoints/Searching.cpp | 36 +- src/lms/ui/PlayQueue.cpp | 7 +- src/lms/ui/explore/ArtistCollector.cpp | 18 +- src/lms/ui/explore/ArtistCollector.hpp | 2 +- src/lms/ui/explore/DatabaseCollectorBase.cpp | 21 +- src/lms/ui/explore/DatabaseCollectorBase.hpp | 4 +- src/lms/ui/explore/Filters.cpp | 2 +- src/lms/ui/explore/PlayQueueController.cpp | 6 +- src/lms/ui/explore/ReleaseCollector.cpp | 17 +- src/lms/ui/explore/ReleaseCollector.hpp | 4 +- src/lms/ui/explore/ReleaseView.cpp | 166 +-- src/lms/ui/explore/ReleasesView.cpp | 182 ++-- src/lms/ui/explore/SearchView.cpp | 9 +- src/lms/ui/explore/TrackCollector.cpp | 17 +- src/lms/ui/explore/TrackCollector.hpp | 4 +- src/lms/ui/explore/TrackListView.cpp | 8 +- src/lms/ui/explore/TracksView.cpp | 182 ++-- 61 files changed, 1704 insertions(+), 2603 deletions(-) diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 91c8c612..773e41f9 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -374,8 +374,8 @@ namespace Cover auto transaction{ session.createSharedTransaction() }; - const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) }; - + // get a track in this release, consider the release is in a single directory + const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) }; if (!tracks.results.empty()) { const Track::pointer& track{ tracks.results.front() }; diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index c9ca60a4..5f8db6eb 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -210,12 +210,11 @@ namespace Database } - RangeResults Artist::findOrphanIds(Session& session, Range range) + RangeResults Artist::findOrphanIds(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; - - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults Artist::findIds(Session& session, const FindParameters& params) @@ -223,7 +222,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } RangeResults Artist::find(Session& session, const FindParameters& params) @@ -231,10 +230,18 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } - RangeResults Artist::findSimilarArtistIds(EnumSet artistLinkTypes, Range range) const + void Artist::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params) }; + Utils::execQuery(query, params.range, func); + } + + RangeResults Artist::findSimilarArtistIds(EnumSet artistLinkTypes, std::optional range) const { assert(session()); @@ -278,7 +285,7 @@ namespace Database for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::vector> Artist::getClusterGroups(std::vector clusterTypes, std::size_t size) const diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp index 675d1691..f93e8de2 100644 --- a/src/libs/services/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -95,7 +95,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } RangeResults Cluster::find(Session& session, const FindParameters& params) @@ -103,15 +103,15 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } - RangeResults Cluster::findOrphans(Session& session, Range range) + RangeResults Cluster::findOrphans(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } Cluster::pointer Cluster::find(Session& session, ClusterId id) @@ -142,14 +142,14 @@ namespace Database _tracks.insert(getDboPtr(track)); } - RangeResults Cluster::getTracks(Range range) const + RangeResults Cluster::getTracks(std::optional range) const { assert(session()); auto query{ session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") .where("c.id = ?").bind(getId()) }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } ClusterType::ClusterType(std::string_view name) @@ -170,7 +170,7 @@ namespace Database } - RangeResults ClusterType::findOrphans(Session& session, Range range) + RangeResults ClusterType::findOrphans(Session& session, std::optional range) { session.checkSharedLocked(); @@ -179,10 +179,10 @@ namespace Database " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") .where("c.id IS NULL") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults ClusterType::findUsed(Session& session, Range range) + RangeResults ClusterType::findUsed(Session& session, std::optional range) { session.checkSharedLocked(); @@ -190,7 +190,7 @@ namespace Database "SELECT DISTINCT c_t.id from cluster_type c_t") .join("cluster c ON c_t.id = c.cluster_type_id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } ClusterType::pointer ClusterType::find(Session& session, std::string_view name) @@ -207,13 +207,13 @@ namespace Database return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } - RangeResults ClusterType::find(Session& session, Range range) + RangeResults ClusterType::find(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT id from cluster_type") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } Cluster::pointer ClusterType::getCluster(const std::string& name) const diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp index d7af57d0..4f645a04 100644 --- a/src/libs/services/database/impl/Listen.cpp +++ b/src/libs/services/database/impl/Listen.cpp @@ -172,7 +172,7 @@ namespace Database if (parameters.syncState) query.where("sync_state = ?").bind(*parameters.syncState); - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime) @@ -187,7 +187,7 @@ namespace Database .resultValue(); } - RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range) { session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) }; @@ -196,57 +196,57 @@ namespace Database .orderBy("COUNT(a.id) DESC") .groupBy("a.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range) { session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range) { session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 8021dc98..5ca1bfa4 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -188,7 +188,7 @@ namespace Database Wt::Dbo::Query createQuery(Session& session, const Release::FindParameters& params) { std::string_view itemToSelect; - + if constexpr (std::is_same_v) itemToSelect = "r.id"; else if constexpr (std::is_same_v>) @@ -256,7 +256,7 @@ namespace Database return session.getDboSession().query("SELECT COUNT(*) FROM release"); } - RangeResults Release::findIdsOrderedByArtist(Session& session, Range range) + RangeResults Release::findIdsOrderedByArtist(Session& session, std::optional range) { session.checkSharedLocked(); @@ -268,15 +268,15 @@ namespace Database " INNER JOIN artist a ON t_a_l.artist_id = a.id") .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Release::findOrphanIds(Session& session, Range range) + RangeResults Release::findOrphanIds(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } RangeResults Release::find(Session& session, const FindParameters& params) @@ -284,7 +284,15 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); + } + + void Release::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params) }; + Utils::execQuery(query, params.range, func); } RangeResults Release::findIds(Session& session, const FindParameters& params) @@ -292,7 +300,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } std::size_t Release::getDiscCount() const diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp index 2f03fbe6..78112ded 100644 --- a/src/libs/services/database/impl/StarredTrack.cpp +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -76,7 +76,7 @@ namespace Database if (params.user.isValid()) query.where("s_t.user_id = ?").bind(params.user); - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index be1f0a16..0be23942 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -44,7 +44,8 @@ namespace Database { session.checkSharedLocked(); - auto query{ session.getDboSession().query(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") }; + std::string selectStatement{ params.distinct ? "SELECT DISTINCT" : "SELECT" }; + auto query{ session.getDboSession().query(selectStatement + " " + std::string{ itemToSelect } + " FROM track t") }; assert(params.keywords.empty() || params.name.empty()); for (std::string_view keyword : params.keywords) @@ -129,7 +130,7 @@ namespace Database query.where("r.name = ?").bind(params.releaseName); } - if (params.trackList.isValid()) + if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList) { query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id"); query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id"); @@ -249,7 +250,7 @@ namespace Database return std::vector(res.begin(), res.end()); } - RangeResults Track::findPaths(Session& session, Range range) + RangeResults Track::findPaths(Session& session, std::optional range) { using QueryResultType = std::tuple; session.checkSharedLocked(); @@ -257,7 +258,7 @@ namespace Database // TODO Dbo traits on filesystem auto query{ session.getDboSession().query("SELECT id, file_path FROM track") }; - RangeResults queryResults{ Utils::execQuery(query, range) }; + RangeResults queryResults{ Utils::execQuery(query, range) }; RangeResults res; res.range = queryResults.range; @@ -267,23 +268,23 @@ namespace Database std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), [](const QueryResultType& queryResult) { - return PathResult{ std::get<0>(queryResult), std::get<1>(queryResult) }; + return PathResult{ std::get(queryResult), std::move(std::get(queryResult)) }; }); return res; } - RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, Range range) + RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, std::optional range) { session.checkSharedLocked(); auto query{ session.getDboSession().query("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } - RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range) + RangeResults Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range) { session.checkSharedLocked(); @@ -291,7 +292,7 @@ namespace Database .where("LENGTH(t.recording_mbid) > 0") .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") }; - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } std::vector Track::getClusters() const @@ -316,7 +317,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, parameters) }; - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } RangeResults Track::find(Session& session, const FindParameters& parameters) @@ -324,10 +325,18 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery>(session, parameters) }; - return Utils::execQuery(query, parameters.range); + return Utils::execQuery(query, parameters.range); } - RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, Range range) + void Track::find(Session& session, const FindParameters& params, std::function func) + { + session.checkSharedLocked(); + + auto query{ createQuery>(session, params)}; + Utils::execQuery(query, params.range, func); + } + + RangeResults Track::findSimilarTrackIds(Session& session, const std::vector& tracks, std::optional range) { assert(!tracks.empty()); session.checkSharedLocked(); @@ -354,7 +363,7 @@ namespace Database for (TrackId trackId : tracks) query.bind(trackId); - return Utils::execQuery(query, range); + return Utils::execQuery(query, range); } void Track::clearArtistLinks() diff --git a/src/libs/services/database/impl/TrackArtistLink.cpp b/src/libs/services/database/impl/TrackArtistLink.cpp index ab64672a..97fda619 100644 --- a/src/libs/services/database/impl/TrackArtistLink.cpp +++ b/src/libs/services/database/impl/TrackArtistLink.cpp @@ -84,7 +84,7 @@ namespace Database session.checkSharedLocked(); auto query{ createQuery(session, params) }; - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } EnumSet TrackArtistLink::findUsedTypes(Session& session) diff --git a/src/libs/services/database/impl/TrackBookmark.cpp b/src/libs/services/database/impl/TrackBookmark.cpp index 71b09708..48dae983 100644 --- a/src/libs/services/database/impl/TrackBookmark.cpp +++ b/src/libs/services/database/impl/TrackBookmark.cpp @@ -27,57 +27,52 @@ namespace Database { -TrackBookmark::TrackBookmark(ObjectPtr user, ObjectPtr track) -: _user {getDboPtr(user)}, -_track {getDboPtr(track)} -{ -} + TrackBookmark::TrackBookmark(ObjectPtr user, ObjectPtr track) + : _user{ getDboPtr(user) }, + _track{ getDboPtr(track) } + { + } -TrackBookmark::pointer -TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) -{ - return session.getDboSession().add(std::unique_ptr {new TrackBookmark {user, track}}); -} + TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) + { + return session.getDboSession().add(std::unique_ptr {new TrackBookmark{ user, track }}); + } -std::size_t -TrackBookmark::getCount(Session& session) -{ - session.checkSharedLocked(); + std::size_t TrackBookmark::getCount(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); -} + return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); + } -RangeResults -TrackBookmark::find(Session& session, UserId userId, Range range) -{ - session.checkSharedLocked(); + RangeResults TrackBookmark::find(Session& session, UserId userId, std::optional range) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT id from track_bookmark") - .where("user_id = ?").bind(userId)}; + auto query{ session.getDboSession().query("SELECT id from track_bookmark") + .where("user_id = ?").bind(userId) }; - return Utils::execQuery(query, range); -} + return Utils::execQuery(query, range); + } -TrackBookmark::pointer -TrackBookmark::find(Session& session, UserId userId, TrackId trackId) -{ - session.checkSharedLocked(); + TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("user_id = ?").bind(userId) - .where("track_id = ?").bind(trackId) - .resultValue(); -} + return session.getDboSession().find() + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) + .resultValue(); + } -TrackBookmark::pointer -TrackBookmark::find(Session& session, TrackBookmarkId id) -{ - session.checkSharedLocked(); + TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); -} + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); + } } // namespace Database diff --git a/src/libs/services/database/impl/TrackFeatures.cpp b/src/libs/services/database/impl/TrackFeatures.cpp index 35f92b27..c7a0293e 100644 --- a/src/libs/services/database/impl/TrackFeatures.cpp +++ b/src/libs/services/database/impl/TrackFeatures.cpp @@ -30,99 +30,92 @@ namespace Database { -TrackFeatures::TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures) -: _data {jsonEncodedFeatures}, -_track {getDboPtr(track)} -{ -} + TrackFeatures::TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures) + : _data{ jsonEncodedFeatures }, + _track{ getDboPtr(track) } + { + } -TrackFeatures::pointer -TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) -{ - return session.getDboSession().add(std::unique_ptr {new TrackFeatures {track, jsonEncodedFeatures}}); -} + TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) + { + return session.getDboSession().add(std::unique_ptr {new TrackFeatures{ track, jsonEncodedFeatures }}); + } -std::size_t -TrackFeatures::getCount(Session& session) -{ - session.checkSharedLocked(); + std::size_t TrackFeatures::getCount(Session& session) + { + session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); -} + return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); + } -TrackFeatures::pointer -TrackFeatures::find(Session& session, TrackFeaturesId id) -{ - session.checkSharedLocked(); + TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("id = ?").bind(id) - .resultValue(); -} + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); + } -TrackFeatures::pointer -TrackFeatures::find(Session& session, TrackId trackId) -{ - session.checkSharedLocked(); + TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId) + { + session.checkSharedLocked(); - return session.getDboSession().find() - .where("track_id = ?").bind(trackId) - .resultValue(); -} + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .resultValue(); + } -RangeResults -TrackFeatures::find(Session& session, Range range) -{ - session.checkSharedLocked(); + RangeResults TrackFeatures::find(Session& session, std::optional range) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT id from track_features")}; + auto query{ session.getDboSession().query("SELECT id from track_features") }; - return Utils::execQuery(query, range); -} + return Utils::execQuery(query, range); + } -FeatureValues -TrackFeatures::getFeatureValues(const FeatureName& featureNode) const -{ - FeatureValuesMap featuresValuesMap {getFeatureValuesMap({featureNode})}; - return std::move(featuresValuesMap[featureNode]); -} + FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const + { + FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({featureNode}) }; + return std::move(featuresValuesMap[featureNode]); + } -FeatureValuesMap -TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const -{ - FeatureValuesMap res; + FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const + { + FeatureValuesMap res; - try - { - std::istringstream iss {_data}; - boost::property_tree::ptree root; + try + { + std::istringstream iss{ _data }; + boost::property_tree::ptree root; - boost::property_tree::read_json(iss, root); + boost::property_tree::read_json(iss, root); - for (const FeatureName& featureName : featureNames) - { - FeatureValues& featureValues {res[featureName]}; + for (const FeatureName& featureName : featureNames) + { + FeatureValues& featureValues{ res[featureName] }; - auto node {root.get_child(featureName)}; + auto node{ root.get_child(featureName) }; - bool hasChildren = false; - for (const auto& child : node.get_child("")) - { - hasChildren = true; - featureValues.push_back(child.second.get_value()); - } + bool hasChildren = false; + for (const auto& child : node.get_child("")) + { + hasChildren = true; + featureValues.push_back(child.second.get_value()); + } - if (!hasChildren) - featureValues.push_back(node.get_value()); - } - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); - res.clear(); - } + if (!hasChildren) + featureValues.push_back(node.get_value()); + } + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); + res.clear(); + } - return res; -} + return res; + } } // namespace Database diff --git a/src/libs/services/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp index 490fc06a..1e98fd6e 100644 --- a/src/libs/services/database/impl/TrackList.cpp +++ b/src/libs/services/database/impl/TrackList.cpp @@ -33,665 +33,299 @@ #include "IdTypeTraits.hpp" #include "Utils.hpp" -namespace Database { - -TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) - : _name {name} -, _type {type} -, _isPublic {isPublic} -, _creationDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())} -, _lastModifiedDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())} -, _user {getDboPtr(user)} +namespace Database { - assert(user); -} - -TrackList::pointer -TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) -{ - return session.getDboSession().add(std::unique_ptr {new TrackList {name, type, isPublic, user}}); -} - -std::size_t -TrackList::getCount(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); -} - - -TrackList::pointer -TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId) -{ - session.checkSharedLocked(); - assert(userId.isValid()); - - return session.getDboSession().find() - .where("name = ?").bind(name) - .where("type = ?").bind(type) - .where("user_id = ?").bind(userId).resultValue(); -} - -RangeResults -TrackList::find(Session& session, const FindParameters& params) -{ - session.checkSharedLocked(); - - auto query {session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l")}; - - if (params.user.isValid()) - query.where("t_l.user_id = ?").bind(params.user); - - if (params.type) - query.where("t_l.type = ?").bind(*params.type); - - if (!params.clusters.empty()) - { - query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); - query.join("track t ON t.id = t_l_e.track_id"); - - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id"; - - WhereClause clusterClause; - for (const ClusterId clusterId : params.clusters) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(clusterId); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; - - query.where(oss.str()); - } - - switch (params.sortMethod) - { - case TrackListSortMethod::None: - break; - case TrackListSortMethod::Name: - query.orderBy("t_l.name COLLATE NOCASE"); - break; - case TrackListSortMethod::LastModifiedDesc: - query.orderBy("t_l.last_modified_date_time DESC"); - break; - } - - return Utils::execQuery(query, params.range); -} - -TrackList::pointer -TrackList::find(Session& session, TrackListId id) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} - -bool -TrackList::isEmpty() const -{ - return _entries.empty(); -} - -std::size_t -TrackList::getCount() const -{ - return _entries.size(); -} - -TrackListEntry::pointer -TrackList::getEntry(std::size_t pos) const -{ - TrackListEntry::pointer res; - - auto entries = getEntries(Range {pos, 1}); - if (!entries.empty()) - res = entries.front(); - - return res; -} - -std::vector -TrackList::getEntries(std::optional range) const -{ - assert(session()); - - auto entries { - session()->find() - .where("tracklist_id = ?").bind(getId()) - .orderBy("id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - return std::vector(entries.begin(), entries.end()); -} - -TrackListEntry::pointer -TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const -{ - assert(session()); - - return session()->find() - .where("tracklist_id = ?").bind(getId()) - .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) - .resultValue(); -} - -static -Wt::Dbo::Query> -createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds, std::optional linkType) -{ - auto query {session.query>(queryStr)}; - query.join("track t ON t.id = t_a_l.track_id"); - query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); - query.join("tracklist_entry p_e ON p_e.track_id = t.id"); - query.join("tracklist p ON p.id = p_e.tracklist_id"); - - query.where("p.id = ?").bind(tracklistId); - - if (linkType) - query.where("t_a_l.type = ?").bind(*linkType); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id" - " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; - - WhereClause clusterClause; - for (auto id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -static -Wt::Dbo::Query> -createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector& clusterIds) -{ - auto query {session.query>(queryStr)}; - query.join("track t ON t.release_id = r.id"); - query.join("tracklist_entry p_e ON p_e.track_id = t.id"); - query.join("tracklist p ON p.id = p_e.tracklist_id"); - - query.where("p.id = ?").bind(tracklistId); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; - - WhereClause clusterClause; - for (ClusterId id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -static -Wt::Dbo::Query> -createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector& clusterIds) -{ - auto query {session.query>("SELECT t from track t INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")}; - - query.where("p.id = ?").bind(tracklistId); - - if (!clusterIds.empty()) - { - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id"; - - WhereClause clusterClause; - for (auto id : clusterIds) - { - clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); - query.bind(id); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; - - query.where(oss.str()); - } - - return query; -} - -std::vector -TrackList::getArtists(const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) - .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1)}; - - switch (sortMethod) - { - case ArtistSortMethod::None: - break; - case ArtistSortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case ArtistSortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - case ArtistSortMethod::Random: - query.orderBy("RANDOM()"); - break; - case ArtistSortMethod::LastWritten: - case ArtistSortMethod::StarredDateDesc: - assert(false); // Not implemented! - break; - } - - Wt::Dbo::collection> collection {query.resultList()}; - - auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - - -std::vector> -TrackList::getReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) - .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector> -TrackList::getTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createTracksQuery(*session(), getId(), clusterIds) - .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) - .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) - .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto collection {createTracksQuery(*session(), getId(), clusterIds) - .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC, p_e.id DESC") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getClusters() const -{ - assert(session()); - - auto res {session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") - .where("p.id = ?").bind(getId()) - .groupBy("c.id") - .orderBy("COUNT(c.id) DESC") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector> -TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const -{ - assert(session()); - std::vector> res; - - if (clusterTypes.empty()) - return res; - - auto query {session()->query>("SELECT c from cluster c")}; - - query.join("track t ON c.id = t_c.cluster_id") - .join("track_cluster t_c ON t_c.track_id = t.id") - .join("cluster_type c_type ON c.cluster_type_id = c_type.id") - .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id") - .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id") - .where("t_l.id = ?").bind(getId()); - - { - std::ostringstream oss; - oss << "c_type.id IN ("; - bool first {true}; - for (auto clusterType : clusterTypes) - { - if (!first) - oss << ", "; - oss << "?"; - query.bind(clusterType ->getId()); - first = false; - } - oss << ")"; - query.where(oss.str()); - } - query.groupBy("c.id"); - query.orderBy("COUNT(c.id) DESC"); - - auto queryRes {query.resultList()}; - - std::map> clustersByType; - for (const Wt::Dbo::ptr& cluster : queryRes) - { - if (clustersByType[cluster->getType()->getId()].size() < size) - clustersByType[cluster->getType()->getId()].push_back(cluster); - } - - for (const auto& [clusterTypeId, clusters] : clustersByType) - res.push_back(clusters); - - return res; -} - -bool -TrackList::hasTrack(TrackId trackId) const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p_e.track_id = ?").bind(trackId) - .where("p.id = ?").bind(getId()); - - return res.size() > 0; -} - -std::vector -TrackList::getSimilarTracks(std::optional offset, std::optional size) const -{ - assert(session()); - - auto res {session()->query>( - "SELECT t FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " WHERE " - " (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)" - " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))" - ) - .bind(getId()) - .bind(getId()) - .groupBy("t.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -TrackList::getTrackIds() const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") - .where("p.id = ?").bind(getId()); - - return std::vector(res.begin(), res.end()); -} - -std::chrono::milliseconds -TrackList::getDuration() const -{ - assert(session()); - - using milli = std::chrono::duration; - - Wt::Dbo::Query query {session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") - .where("p_e.tracklist_id = ?").bind(getId())}; - - return query.resultValue(); -} - -void -TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) -{ - _lastModifiedDateTime = Utils::normalizeDateTime(dateTime); -} - -std::vector -TrackList::getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)}; - - auto collection {query - .orderBy("COUNT(a.id) DESC") - .groupBy("a.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)}; - auto collection {query - .orderBy("COUNT(r.id) DESC") - .groupBy("r.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -TrackList::getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const -{ - assert(session()); - - auto query {createTracksQuery(*session(), getId(), clusterIds)}; - auto collection {query - .orderBy("COUNT(t.id) DESC") - .groupBy("t.id") - .limit(range ? static_cast(range->size) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->size) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -: _dateTime {Utils::normalizeDateTime(dateTime)} -, _track {getDboPtr(track)} -, _tracklist {getDboPtr(tracklist)} -{ - assert(track); - assert(tracklist); -} - -TrackListEntry::pointer -TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -{ - return session.getDboSession().add(std::unique_ptr {new TrackListEntry {track, tracklist, dateTime}}); -} - -void -TrackListEntry::onPostCreated() -{ - _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); -} - -void -TrackListEntry::onPreRemove() -{ - _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); -} - -TrackListEntry::pointer -TrackListEntry::getById(Session& session, TrackListEntryId id) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("id = ?").bind(id).resultValue(); -} + TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) + : _name{ name } + , _type{ type } + , _isPublic{ isPublic } + , _creationDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) } + , _lastModifiedDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) } + , _user{ getDboPtr(user) } + { + assert(user); + } + + TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) + { + return session.getDboSession().add(std::unique_ptr {new TrackList{ name, type, isPublic, user }}); + } + + std::size_t TrackList::getCount(Session& session) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); + } + + + TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId) + { + session.checkSharedLocked(); + assert(userId.isValid()); + + return session.getDboSession().find() + .where("name = ?").bind(name) + .where("type = ?").bind(type) + .where("user_id = ?").bind(userId).resultValue(); + } + + RangeResults TrackList::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); + + auto query{ session.getDboSession().query("SELECT DISTINCT t_l.id FROM tracklist t_l") }; + + if (params.user.isValid()) + query.where("t_l.user_id = ?").bind(params.user); + + if (params.type) + query.where("t_l.type = ?").bind(*params.type); + + if (!params.clusters.empty()) + { + query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); + query.join("track t ON t.id = t_l_e.track_id"); + + std::ostringstream oss; + oss << "t.id IN (SELECT DISTINCT t.id FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id"; + + WhereClause clusterClause; + for (const ClusterId clusterId : params.clusters) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(clusterId); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; + + query.where(oss.str()); + } + + switch (params.sortMethod) + { + case TrackListSortMethod::None: + break; + case TrackListSortMethod::Name: + query.orderBy("t_l.name COLLATE NOCASE"); + break; + case TrackListSortMethod::LastModifiedDesc: + query.orderBy("t_l.last_modified_date_time DESC"); + break; + } + + return Utils::execQuery(query, params.range); + } + + TrackList::pointer TrackList::find(Session& session, TrackListId id) + { + session.checkSharedLocked(); + + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + bool TrackList::isEmpty() const + { + return _entries.empty(); + } + + std::size_t TrackList::getCount() const + { + return _entries.size(); + } + + TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const + { + TrackListEntry::pointer res; + + auto entries = getEntries(Range{ pos, 1 }); + if (!entries.empty()) + res = entries.front(); + + return res; + } + + std::vector TrackList::getEntries(std::optional range) const + { + assert(session()); + + auto entries{ + session()->find() + .where("tracklist_id = ?").bind(getId()) + .orderBy("id") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList() }; + + return std::vector(entries.begin(), entries.end()); + } + + TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const + { + assert(session()); + + return session()->find() + .where("tracklist_id = ?").bind(getId()) + .where("track_id = ?").bind(track->getId()) + .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) + .resultValue(); + } + + std::vector TrackList::getClusters() const + { + assert(session()); + + auto res{ session()->query>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") + .where("p.id = ?").bind(getId()) + .groupBy("c.id") + .orderBy("COUNT(c.id) DESC") + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + std::vector> TrackList::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const + { + assert(session()); + std::vector> res; + + if (clusterTypes.empty()) + return res; + + auto query{ session()->query>("SELECT c from cluster c") }; + + query.join("track t ON c.id = t_c.cluster_id") + .join("track_cluster t_c ON t_c.track_id = t.id") + .join("cluster_type c_type ON c.cluster_type_id = c_type.id") + .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id") + .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id") + .where("t_l.id = ?").bind(getId()); + + { + std::ostringstream oss; + oss << "c_type.id IN ("; + bool first{ true }; + for (auto clusterType : clusterTypes) + { + if (!first) + oss << ", "; + oss << "?"; + query.bind(clusterType->getId()); + first = false; + } + oss << ")"; + query.where(oss.str()); + } + query.groupBy("c.id"); + query.orderBy("COUNT(c.id) DESC"); + + auto queryRes{ query.resultList() }; + + std::map> clustersByType; + for (const Wt::Dbo::ptr& cluster : queryRes) + { + if (clustersByType[cluster->getType()->getId()].size() < size) + clustersByType[cluster->getType()->getId()].push_back(cluster); + } + + for (const auto& [clusterTypeId, clusters] : clustersByType) + res.push_back(clusters); + + return res; + } + + bool TrackList::hasTrack(TrackId trackId) const + { + assert(session()); + + Wt::Dbo::collection res = session()->query("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p_e.track_id = ?").bind(trackId) + .where("p.id = ?").bind(getId()); + + return res.size() > 0; + } + + std::vector TrackList::getSimilarTracks(std::optional offset, std::optional size) const + { + assert(session()); + + auto res{ session()->query>( + "SELECT t FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " WHERE " + " (t_c.cluster_id IN (SELECT DISTINCT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id WHERE p.id = ?)" + " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))" + ) + .bind(getId()) + .bind(getId()) + .groupBy("t.id") + .orderBy("COUNT(*) DESC, RANDOM()") + .limit(size ? static_cast(*size) : -1) + .offset(offset ? static_cast(*offset) : -1) + .resultList() }; + + return std::vector(res.begin(), res.end()); + } + + std::vector TrackList::getTrackIds() const + { + assert(session()); + + Wt::Dbo::collection res = session()->query("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") + .where("p.id = ?").bind(getId()); + + return std::vector(res.begin(), res.end()); + } + + std::chrono::milliseconds TrackList::getDuration() const + { + assert(session()); + + using milli = std::chrono::duration; + + Wt::Dbo::Query query{ session()->query("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") + .where("p_e.tracklist_id = ?").bind(getId()) }; + + return query.resultValue(); + } + + void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) + { + _lastModifiedDateTime = Utils::normalizeDateTime(dateTime); + } + + TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) + : _dateTime{ Utils::normalizeDateTime(dateTime) } + , _track{ getDboPtr(track) } + , _tracklist{ getDboPtr(tracklist) } + { + assert(track); + assert(tracklist); + } + + TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) + { + return session.getDboSession().add(std::unique_ptr {new TrackListEntry{ track, tracklist, dateTime }}); + } + + void TrackListEntry::onPostCreated() + { + _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); + } + + void TrackListEntry::onPreRemove() + { + _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())); + } + + TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id) + { + session.checkSharedLocked(); + + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } } // namespace Database diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index 926b4ac3..ca833435 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -58,7 +58,7 @@ namespace Database { if (params.feedbackBackend) query.where("feedback_backend = ?").bind(*params.feedbackBackend); - return Utils::execQuery(query, params.range); + return Utils::execQuery(query, params.range); } User::pointer User::findDemoUser(Session& session) diff --git a/src/libs/services/database/impl/Utils.hpp b/src/libs/services/database/impl/Utils.hpp index 213bce59..85623814 100644 --- a/src/libs/services/database/impl/Utils.hpp +++ b/src/libs/services/database/impl/Utils.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -30,57 +31,54 @@ namespace Database::Utils { #define ESCAPE_CHAR_STR "\\" - static inline constexpr char escapeChar {'\\'}; - std::string escapeLikeKeyword(std::string_view keywords); + static inline constexpr char escapeChar{ '\\' }; + std::string escapeLikeKeyword(std::string_view keywords); - template - RangeResults - execQuery(Wt::Dbo::Query& query, Range range) - { - RangeResults res; + template + void applyRange(Query& query, std::optional range) + { + if (range) + { + query.limit(static_cast(range->size)); + query.offset(static_cast(range->offset)); + } + } - auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) - .offset(range.offset ? static_cast(range.offset) : -1) - .resultList()}; + template + RangeResults execQuery(Query& query, std::optional range) + { + RangeResults res; - res.results.assign(collection.begin(), collection.end()); - if (range.size && res.results.size() == static_cast(range.size) + 1) - { - res.moreResults = true; - res.results.pop_back(); - } - else - res.moreResults = false; + if (range) + applyRange(query, Range{ range->offset, range->size + 1 }); - res.range.offset = range.offset; - res.range.size = res.results.size(); - return res; - } + auto collection{ query.resultList() }; + res.results.assign(collection.begin(), collection.end()); + if (range && res.results.size() == static_cast(range->size) + 1) + { + // TODO may optim by not actually requesting the last one + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; - template - RangeResults - execQuery(Wt::Dbo::Query>& query, Range range) - { - RangeResults res; + res.range.offset = range->offset; + res.range.size = res.results.size(); - auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) - .offset(range.offset ? static_cast(range.offset) : -1) - .resultList()}; + return res; + } - res.results.assign(collection.begin(), collection.end()); - if (range.size && res.results.size() == static_cast(range.size) + 1) - { - res.moreResults = true; - res.results.pop_back(); - } - else - res.moreResults = false; + template + void execQuery(Query& query, std::optional range, std::function func) + { + if (range) + applyRange(query, range); - res.range.offset = range.offset; - res.range.size = res.results.size(); - return res; - } + for (const auto& res : query.resultList()) + func(res); + } - Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); + Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); } // namespace Database::Utils diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index 7fcaadac..e561df6b 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -58,7 +58,7 @@ namespace Database std::vector keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) std::optional linkType; // if set, only artists that have produced at least one track with this link type ArtistSortMethod sortMethod{ ArtistSortMethod::None }; - Range range; + std::optional range; Wt::WDateTime writtenAfter; UserId starringUser; // only artists starred by this user std::optional feedbackBackend; // and for this feedback backend @@ -69,7 +69,7 @@ namespace Database FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } @@ -84,8 +84,9 @@ namespace Database static pointer find(Session& session, ArtistId id); static std::vector find(Session& session, const std::string& name); // exact match on name field static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); - static RangeResults findOrphanIds(Session& session, Range range); // No track related + static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // No track related static bool exists(Session& session, ArtistId id); // Accessors @@ -94,7 +95,7 @@ namespace Database std::optional getMBID() const { return UUID::fromString(_MBID); } // No artistLinkTypes means get them all - RangeResults findSimilarArtistIds(EnumSet artistLinkTypes = {}, Range range = {}) const; + RangeResults findSimilarArtistIds(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; // Get the cluster of the tracks made by this artist // Each clusters are grouped by cluster type, sorted by the number of occurence diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp index fa7c1c0a..7220d647 100644 --- a/src/libs/services/database/include/services/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -45,12 +46,12 @@ namespace Database { public: struct FindParameters { - Range range; - ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type - TrackId track; // if set, clusters involved in this track - ReleaseId release; // if set, clusters involved in this release + std::optional range; + ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type + TrackId track; // if set, clusters involved in this track + ReleaseId release; // if set, clusters involved in this release - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } @@ -60,11 +61,11 @@ namespace Database { // Find utility static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& range); - static RangeResults find(Session& session, const FindParameters& range); - static void find(Session& session, const FindParameters& range, std::function _func); - static pointer find(Session& session, ClusterId id); - static RangeResults findOrphans(Session& session, Range range); + static RangeResults findIds(Session& session, const FindParameters& params); + static RangeResults find(Session& session, const FindParameters& params); + static void find(Session& session, const FindParameters& params, std::function _func); + static pointer find(Session& session, ClusterId id); + static RangeResults findOrphans(Session& session, std::optional range = std::nullopt); // May be very slow static std::size_t computeTrackCount(Session& session, ClusterId id); @@ -74,7 +75,7 @@ namespace Database { std::string_view getName() const { return _name; } ObjectPtr getType() const { return _clusterType; } std::size_t getTracksCount() const { return _trackCount; } - RangeResults getTracks(Range range) const; + RangeResults getTracks(std::optional range = std::nullopt) const; std::size_t getReleasesCount() const { return _releaseCount; }; void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; } @@ -116,11 +117,11 @@ namespace Database { // Getters static std::size_t getCount(Session& session); - static RangeResults find(Session& session, Range range); + static RangeResults find(Session& session, std::optional range = std::nullopt); static pointer find(Session& session, std::string_view name); static pointer find(Session& session, ClusterTypeId id); - static RangeResults findOrphans(Session& session, Range range); - static RangeResults findUsed(Session& session, Range range); + static RangeResults findOrphans(Session& session, std::optional range = std::nullopt); + static RangeResults findUsed(Session& session, std::optional range = std::nullopt); static void remove(Session& session, const std::string& name); diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp index 20930bc8..66eadcf3 100644 --- a/src/libs/services/database/include/services/database/Listen.hpp +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -19,6 +19,8 @@ #pragma once +#include + #include #include @@ -45,10 +47,10 @@ namespace Database struct FindParameters { - UserId user; - std::optional backend; - std::optional syncState; - Range range; + UserId user; + std::optional backend; + std::optional syncState; + std::optional range; FindParameters& setUser(UserId _user) { user = _user; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; } @@ -63,13 +65,13 @@ namespace Database static RangeResults find(Session& session, const FindParameters& parameters); // Stats - static RangeResults getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); - static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); - static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); + static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); + static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); - static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); - static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); - static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); + static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); + static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId); diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 28035a38..2c943227 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -53,7 +53,7 @@ namespace Database std::vector clusters; // if non empty, releases that belong to these clusters std::vector keywords; // if non empty, name must match all of these keywords ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; - Range range; + std::optional range; Wt::WDateTime writtenAfter; std::optional dateRange; UserId starringUser; // only releases starred by this user @@ -67,7 +67,7 @@ namespace Database FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setDateRange(const std::optional& _dateRange) { dateRange = _dateRange; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } @@ -89,9 +89,10 @@ namespace Database static std::vector find(Session& session, const std::string& name); static pointer find(Session& session, ReleaseId id); static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); - static RangeResults findOrphanIds(Session& session, Range range); // not track related - static RangeResults findIdsOrderedByArtist(Session& session, Range range); + static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // not track related + static RangeResults findIdsOrderedByArtist(Session& session, std::optional range = std::nullopt); // Get the cluster of the tracks that belong to this release // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) diff --git a/src/libs/services/database/include/services/database/StarredTrack.hpp b/src/libs/services/database/include/services/database/StarredTrack.hpp index 7b3e50f7..5db11aaa 100644 --- a/src/libs/services/database/include/services/database/StarredTrack.hpp +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -45,11 +45,11 @@ namespace Database std::optional backend; // for this backend std::optional syncState; // and these states UserId user; // and this user - Range range; + std::optional range; FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } }; // Search utility diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index e15aaf1e..07de5a7c 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -46,207 +46,208 @@ namespace Database { -class Artist; -class Cluster; -class ClusterType; -class Release; -class Session; -class TrackArtistLink; -class TrackStats; -class User; + class Artist; + class Cluster; + class ClusterType; + class Release; + class Session; + class TrackArtistLink; + class TrackStats; + class User; -class Track final : public Object -{ - public: - struct FindParameters - { - std::vector clusters; // if non empty, tracks that belong to these clusters - std::vector keywords; // if non empty, name must match all of these keywords - std::string name; // if non empty, must match this name - TrackSortMethod sortMethod {TrackSortMethod::None}; - Range range; - Wt::WDateTime writtenAfter; - UserId starringUser; // only tracks starred by this user - std::optional feedbackBackend; // and for this feedback backend - ArtistId artist; // only tracks that involve this artist - std::string artistName; // only tracks that involve this artist name - EnumSet trackArtistLinkTypes; // and for these link types - bool nonRelease {}; // only tracks that do not belong to a release - ReleaseId release; // matching this release - std::string releaseName; // matching this release name - TrackListId trackList; // matching this trackList - std::optional trackNumber; // matching this track number - bool distinct {true}; + class Track final : public Object + { + public: + struct FindParameters + { + std::vector clusters; // if non empty, tracks that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + std::string name; // if non empty, must match this name + TrackSortMethod sortMethod{ TrackSortMethod::None }; + std::optional range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only tracks starred by this user + std::optional feedbackBackend; // and for this feedback backend + ArtistId artist; // only tracks that involve this artist + std::string artistName; // only tracks that involve this artist name + EnumSet trackArtistLinkTypes; // and for these link types + bool nonRelease{}; // only tracks that do not belong to a release + ReleaseId release; // matching this release + std::string releaseName; // matching this release name + TrackListId trackList; // matching this trackList + std::optional trackNumber; // matching this track number + bool distinct{ true }; - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } - FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } - FindParameters& setName(std::string_view _name) { name = _name; return *this; } - FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } - FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } - FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } - FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } - FindParameters& setArtistName(std::string_view _artistName, EnumSet _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } - FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } - FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } - FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; } - FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } - FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; } - FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; } - }; + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setName(std::string_view _name) { name = _name; return *this; } + FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } + FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } + FindParameters& setArtistName(std::string_view _artistName, EnumSet _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } + FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } + FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } + FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; } + FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } + FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; } + FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; } + }; - struct PathResult - { - TrackId trackId; - std::filesystem::path path; - }; + struct PathResult + { + TrackId trackId; + std::filesystem::path path; + }; - Track() = default; + Track() = default; - // Find utility functions - static std::size_t getCount(Session& session); - static pointer findByPath(Session& session, const std::filesystem::path& p); - static pointer find(Session& session, TrackId id); - 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 findSimilarTrackIds(Session& session, const std::vector& trackIds, Range range); + // Find utility functions + static std::size_t getCount(Session& session); + static pointer findByPath(Session& session, const std::filesystem::path& p); + static pointer find(Session& session, TrackId id); + 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 findSimilarTrackIds(Session& session, const std::vector& trackIds, std::optional range = std::nullopt); - 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 findIdsTrackMBIDDuplicates(Session& session, Range range); - static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range); + static RangeResults findIds(Session& session, const FindParameters& parameters); + static RangeResults find(Session& session, const FindParameters& parameters); + static void find(Session& session, const FindParameters& parameters, std::function func); + static RangeResults findPaths(Session& session, std::optional range = std::nullopt); + static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); + static RangeResults findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional range = std::nullopt); - // Accessors - void setScanVersion(std::size_t version) { _scanVersion = version; } - void setTrackNumber(std::optional num) { _trackNumber = num; } - void setDiscNumber(std::optional num) { _discNumber = num; } - void setTotalTrack(std::optional totalTrack) { _totalTrack = totalTrack; } - void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } - void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } - void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } - void setDuration(std::chrono::milliseconds duration) { _duration = duration; } - void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } - void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } - void setDate(const Wt::WDate& date) { _date = date; } - void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } - void setHasCover(bool hasCover) { _hasCover = hasCover; } - void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } - void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } - void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } - void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } - void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } - void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! - void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } - void clearArtistLinks(); - void addArtistLink(const ObjectPtr& artistLink); - void setRelease(ObjectPtr release) { _release = getDboPtr(release); } - void setClusters(const std::vector>& clusters ); + // Accessors + void setScanVersion(std::size_t version) { _scanVersion = version; } + void setTrackNumber(std::optional num) { _trackNumber = num; } + void setDiscNumber(std::optional num) { _discNumber = num; } + void setTotalTrack(std::optional totalTrack) { _totalTrack = totalTrack; } + void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } + void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } + void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } + void setDuration(std::chrono::milliseconds duration) { _duration = duration; } + void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } + void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } + void setDate(const Wt::WDate& date) { _date = date; } + void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } + void setHasCover(bool hasCover) { _hasCover = hasCover; } + void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } + void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } + void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } + void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } + void setTrackReplayGain(std::optional replayGain) { _trackReplayGain = replayGain; } + void setReleaseReplayGain(std::optional replayGain) { _releaseReplayGain = replayGain; } // may be by disc! + void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } + void clearArtistLinks(); + void addArtistLink(const ObjectPtr& artistLink); + void setRelease(ObjectPtr release) { _release = getDboPtr(release); } + void setClusters(const std::vector>& clusters); - std::size_t getScanVersion() const { return _scanVersion; } - std::optional getTrackNumber() const { return _trackNumber; } - std::optional getTotalTrack() const { return _totalTrack; } - std::optional getDiscNumber() const { return _discNumber; } - const std::string& getDiscSubtitle() const { return _discSubtitle; } - std::string getName() const { return _name; } - std::filesystem::path getPath() const { return _filePath; } - std::chrono::milliseconds getDuration() const { return _duration; } - const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } - std::optional getYear() const; - std::optional getOriginalYear() const; - Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } - Wt::WDateTime getAddedTime() const { return _fileAdded; } - bool hasCover() const { return _hasCover; } - std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } - std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } - std::optional getCopyright() const; - std::optional getCopyrightURL() const; - std::optional getTrackReplayGain() const { return _trackReplayGain; } - std::optional getReleaseReplayGain() const { return _releaseReplayGain; } - std::string_view getArtistDisplayName() const { return _artistDisplayName; } - // no artistLinkTypes means get all - std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all - std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all - std::vector> getArtistLinks() const; - ObjectPtr getRelease() const { return _release; } - std::vector> getClusters() const; - std::vector getClusterIds() const; + std::size_t getScanVersion() const { return _scanVersion; } + std::optional getTrackNumber() const { return _trackNumber; } + std::optional getTotalTrack() const { return _totalTrack; } + std::optional getDiscNumber() const { return _discNumber; } + const std::string& getDiscSubtitle() const { return _discSubtitle; } + std::string getName() const { return _name; } + std::filesystem::path getPath() const { return _filePath; } + std::chrono::milliseconds getDuration() const { return _duration; } + const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } + std::optional getYear() const; + std::optional getOriginalYear() const; + Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } + Wt::WDateTime getAddedTime() const { return _fileAdded; } + bool hasCover() const { return _hasCover; } + std::optional getTrackMBID() const { return UUID::fromString(_trackMBID); } + std::optional getRecordingMBID() const { return UUID::fromString(_recordingMBID); } + std::optional getCopyright() const; + std::optional getCopyrightURL() const; + std::optional getTrackReplayGain() const { return _trackReplayGain; } + std::optional getReleaseReplayGain() const { return _releaseReplayGain; } + std::string_view getArtistDisplayName() const { return _artistDisplayName; } + // no artistLinkTypes means get all + std::vector> getArtists(EnumSet artistLinkTypes) const; // no type means all + std::vector getArtistIds(EnumSet artistLinkTypes) const; // no type means all + std::vector> getArtistLinks() const; + ObjectPtr getRelease() const { return _release; } + std::vector> getClusters() const; + std::vector getClusterIds() const; - std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - template - void persist(Action& a) - { - Wt::Dbo::field(a, _scanVersion, "scan_version"); - Wt::Dbo::field(a, _trackNumber, "track_number"); - Wt::Dbo::field(a, _discNumber, "disc_number"); - Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _duration, "duration"); - Wt::Dbo::field(a, _date, "date"); - Wt::Dbo::field(a, _originalDate, "original_date"); - Wt::Dbo::field(a, _filePath, "file_path"); - Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); - Wt::Dbo::field(a, _fileAdded, "file_added"); - Wt::Dbo::field(a, _hasCover, "has_cover"); - Wt::Dbo::field(a, _trackMBID, "mbid"); - Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); - Wt::Dbo::field(a, _copyright, "copyright"); - Wt::Dbo::field(a, _copyrightURL, "copyright_url"); - Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); - Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) - Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); - Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); - Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); - } + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scanVersion, "scan_version"); + Wt::Dbo::field(a, _trackNumber, "track_number"); + Wt::Dbo::field(a, _discNumber, "disc_number"); + Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _duration, "duration"); + Wt::Dbo::field(a, _date, "date"); + Wt::Dbo::field(a, _originalDate, "original_date"); + Wt::Dbo::field(a, _filePath, "file_path"); + Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); + Wt::Dbo::field(a, _fileAdded, "file_added"); + Wt::Dbo::field(a, _hasCover, "has_cover"); + Wt::Dbo::field(a, _trackMBID, "mbid"); + Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); + Wt::Dbo::field(a, _copyright, "copyright"); + Wt::Dbo::field(a, _copyrightURL, "copyright_url"); + Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); + Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) + Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); + Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); + } - private: - friend class ::Database::Session; - Track(const std::filesystem::path& p); - static pointer create(Session& session, const std::filesystem::path& p); + private: + friend class ::Database::Session; + Track(const std::filesystem::path& p); + static pointer create(Session& session, const std::filesystem::path& p); - static constexpr std::size_t _maxNameLength {128}; - static constexpr std::size_t _maxCopyrightLength {128}; - static constexpr std::size_t _maxCopyrightURLLength {128}; + static constexpr std::size_t _maxNameLength{ 128 }; + static constexpr std::size_t _maxCopyrightLength{ 128 }; + static constexpr std::size_t _maxCopyrightURLLength{ 128 }; - int _scanVersion {}; - std::optional _trackNumber {}; - std::optional _discNumber {}; - std::optional _totalTrack {}; - std::string _discSubtitle; - std::string _name; - std::chrono::duration _duration {}; - Wt::WDate _date; - Wt::WDate _originalDate; - std::string _filePath; - Wt::WDateTime _fileLastWrite; - Wt::WDateTime _fileAdded; - bool _hasCover {}; - std::string _trackMBID; - std::string _recordingMBID; - std::string _copyright; - std::string _copyrightURL; - std::optional _trackReplayGain; - std::optional _releaseReplayGain; - std::string _artistDisplayName; + int _scanVersion{}; + std::optional _trackNumber{}; + std::optional _discNumber{}; + std::optional _totalTrack{}; + std::string _discSubtitle; + std::string _name; + std::chrono::duration _duration{}; + Wt::WDate _date; + Wt::WDate _originalDate; + std::string _filePath; + Wt::WDateTime _fileLastWrite; + Wt::WDateTime _fileAdded; + bool _hasCover{}; + std::string _trackMBID; + std::string _recordingMBID; + std::string _copyright; + std::string _copyrightURL; + std::optional _trackReplayGain; + std::optional _releaseReplayGain; + std::string _artistDisplayName; - Wt::Dbo::ptr _release; - Wt::Dbo::collection> _trackArtistLinks; - Wt::Dbo::collection> _clusters; -}; + Wt::Dbo::ptr _release; + Wt::Dbo::collection> _trackArtistLinks; + Wt::Dbo::collection> _clusters; + }; -namespace Debug -{ - struct TrackInfo - { - Session& session; - TrackId trackId; - }; - std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); -} + namespace Debug + { + struct TrackInfo + { + Session& session; + TrackId trackId; + }; + std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); + } } // namespace database diff --git a/src/libs/services/database/include/services/database/TrackArtistLink.hpp b/src/libs/services/database/include/services/database/TrackArtistLink.hpp index a1dec682..657d6ae3 100644 --- a/src/libs/services/database/include/services/database/TrackArtistLink.hpp +++ b/src/libs/services/database/include/services/database/TrackArtistLink.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -45,13 +46,13 @@ namespace Database public: struct FindParameters { - Range range; + std::optional range; std::optional linkType; // if set, only artists that have produced at least one track with this link type ArtistId artist; // if set, links involved with this artist ReleaseId release; // if set, artists involved in this release TrackId track; // if set, artists involved in this track - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } diff --git a/src/libs/services/database/include/services/database/TrackBookmark.hpp b/src/libs/services/database/include/services/database/TrackBookmark.hpp index fd8ae8df..15aaaf07 100644 --- a/src/libs/services/database/include/services/database/TrackBookmark.hpp +++ b/src/libs/services/database/include/services/database/TrackBookmark.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include @@ -45,7 +46,7 @@ class TrackBookmark final : public Object // Find utility functions static std::size_t getCount(Session& session); static pointer find(Session& session, TrackBookmarkId id); - static RangeResults find(Session& session, UserId userId, Range range); + static RangeResults find(Session& session, UserId userId, std::optional range = std::nullopt); static pointer find(Session& session, UserId userId, TrackId trackId); // Setters diff --git a/src/libs/services/database/include/services/database/TrackFeatures.hpp b/src/libs/services/database/include/services/database/TrackFeatures.hpp index 93dcbcd6..5c7715ec 100644 --- a/src/libs/services/database/include/services/database/TrackFeatures.hpp +++ b/src/libs/services/database/include/services/database/TrackFeatures.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include #include @@ -51,7 +52,7 @@ class TrackFeatures final : public Object static std::size_t getCount(Session& session); static pointer find(Session& session, TrackFeaturesId id); static pointer find(Session& session, TrackId trackId); - static RangeResults find(Session& session, Range range); + static RangeResults find(Session& session, std::optional range = std::nullopt); FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; diff --git a/src/libs/services/database/include/services/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp index ce84c1a4..dc29ef33 100644 --- a/src/libs/services/database/include/services/database/TrackList.hpp +++ b/src/libs/services/database/include/services/database/TrackList.hpp @@ -36,151 +36,141 @@ namespace Database { -class Artist; -class Cluster; -class ClusterType; -class Release; -class Session; -class Track; -class TrackListEntry; -class User; + class Artist; + class Cluster; + class ClusterType; + class Release; + class Session; + class Track; + class TrackListEntry; + class User; -class TrackList final : public Object -{ - public: - TrackList() = default; + class TrackList final : public Object + { + public: + TrackList() = default; - // Stats utility - std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + // Search utility + struct FindParameters + { + std::vector clusters; // if non empty, tracklists that have tracks that belong to these clusters + std::optional range; + std::optional type; + UserId user; // only tracklists owned by this user + TrackListSortMethod sortMethod{ TrackListSortMethod::None }; - // Search utility - struct FindParameters - { - std::vector clusters; // if non empty, tracklists that have tracks that belong to these clusters - Range range; - std::optional type; - UserId user; // only tracklists owned by this user - TrackListSortMethod sortMethod {TrackListSortMethod::None}; + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + FindParameters& setType(TrackListType _type) { type = _type; return *this; } + FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } + }; + static std::size_t getCount(Session& session); + static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); + static pointer find(Session& session, TrackListId tracklistId); + static RangeResults find(Session& session, const FindParameters& params); - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } - FindParameters& setType(TrackListType _type) { type = _type; return *this; } - FindParameters& setUser(UserId _user) { user = _user; return *this; } - FindParameters& setSortMethod(TrackListSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } - }; - static std::size_t getCount(Session& session); - static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); - static pointer find(Session& session, TrackListId tracklistId); - static RangeResults find(Session& session, const FindParameters& params); + // Accessors + std::string_view getName() const { return _name; } + bool isPublic() const { return _isPublic; } + TrackListType getType() const { return _type; } + ObjectPtr getUser() const { return _user; } - // Accessors - std::string_view getName() const { return _name; } - bool isPublic() const { return _isPublic; } - TrackListType getType() const { return _type; } - ObjectPtr getUser() const { return _user; } + // Modifiers + void setName(const std::string& name) { _name = name; } + void setIsPublic(bool isPublic) { _isPublic = isPublic; } + void clear() { _entries.clear(); } - // Modifiers - void setName(const std::string& name) { _name = name; } - void setIsPublic(bool isPublic) { _isPublic = isPublic; } - void clear() { _entries.clear(); } + // Get tracks, ordered by position + bool isEmpty() const; + std::size_t getCount() const; + ObjectPtr getEntry(std::size_t pos) const; + std::vector> getEntries(std::optional range = {}) const; + ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; - // Get tracks, ordered by position - bool isEmpty() const; - std::size_t getCount() const; - ObjectPtr getEntry(std::size_t pos) const; - std::vector> getEntries(std::optional range = {}) const; - ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; + RangeResults> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; + RangeResults> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; + RangeResults> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; - std::vector> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; + std::vector getTrackIds() const; + std::chrono::milliseconds getDuration() const; - // Sorted by date time - std::vector> getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + void setLastModifiedDateTime(const Wt::WDateTime& dateTime); - std::vector getTrackIds() const; - std::chrono::milliseconds getDuration() const; + // Get clusters, order by occurence + std::vector> getClusters() const; + std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; - void setLastModifiedDateTime(const Wt::WDateTime& dateTime); + bool hasTrack(TrackId trackId) const; - // Get clusters, order by occurence - std::vector> getClusters() const; - std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; + // Ordered from most clusters in common + std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; - bool hasTrack(TrackId trackId) const; + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _isPublic, "public"); + Wt::Dbo::field(a, _creationDateTime, "creation_date_time"); + Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time"); - // Ordered from most clusters in common - std::vector> getSimilarTracks(std::optional offset = {}, std::optional size = {}) const; + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); + } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _name, "name"); - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _isPublic, "public"); - Wt::Dbo::field(a, _creationDateTime, "creation_date_time"); - Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time"); + private: + friend class Session; + TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); - Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); - } + std::string _name; + TrackListType _type{ TrackListType::Playlist }; + bool _isPublic{ false }; + Wt::WDateTime _creationDateTime; + Wt::WDateTime _lastModifiedDateTime; - private: - friend class Session; - TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); - static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr user); + Wt::Dbo::ptr _user; + Wt::Dbo::collection> _entries; + }; - std::string _name; - TrackListType _type {TrackListType::Playlist}; - bool _isPublic {false}; - Wt::WDateTime _creationDateTime; - Wt::WDateTime _lastModifiedDateTime; + class TrackListEntry final : public Object + { + public: + TrackListEntry() = default; - Wt::Dbo::ptr _user; - Wt::Dbo::collection> _entries; -}; + bool hasOnPostCreated() const override { return true; } + void onPostCreated() override; -class TrackListEntry final : public Object -{ - public: - TrackListEntry() = default; + bool hasOnPreRemove() const override { return true; } + void onPreRemove() override; - bool hasOnPostCreated() const override { return true; } - void onPostCreated() override; + // find utility + static pointer getById(Session& session, TrackListEntryId id); - bool hasOnPreRemove() const override { return true; } - void onPreRemove() override; + // Accessors + ObjectPtr getTrack() const { return _track; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } - // find utility - static pointer getById(Session& session, TrackListEntryId id); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _dateTime, "date_time"); - // Accessors - ObjectPtr getTrack() const { return _track; } - const Wt::WDateTime& getDateTime() const { return _dateTime; } + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade); + } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _dateTime, "date_time"); + private: + friend class Session; + TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr tracklist); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); - Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade); - } - - private: - friend class Session; - TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); - TrackListEntry(ObjectPtr track, ObjectPtr tracklist); - static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); - - Wt::WDateTime _dateTime; // optional date time - Wt::Dbo::ptr _track; - Wt::Dbo::ptr _tracklist; -}; + Wt::WDateTime _dateTime; // optional date time + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _tracklist; + }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index 881d3f97..5cc16877 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -29,7 +29,7 @@ namespace Database // Caution: do not change enum values if they are set! // Request: - // size = 0 => no size limit! + // size = 0 => means we don't want data // Response (via RangeResults) // size => results size struct Range @@ -37,8 +37,7 @@ namespace Database std::size_t offset{}; std::size_t size{}; - // TODO remove this - operator bool() const { return size != 0; } + bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; } }; // Func must return true to continue iterating diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index f177120f..42938e4f 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -49,11 +49,11 @@ namespace Database { { std::optional scrobblingBackend; std::optional feedbackBackend; - Range range; + std::optional range; FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; } - FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } }; static inline constexpr std::size_t MinNameLength{ 3 }; diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index 6e9020fd..b3127c04 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -51,7 +51,7 @@ TEST_F(DatabaseFixture, Artist) ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::findOrphanIds(session, Range{}); + artists = Artist::findOrphanIds(session); ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); } @@ -64,6 +64,18 @@ TEST_F(DatabaseFixture, Artist) ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front()->getId(), artist.getId()); } + + { + auto transaction{ session.createSharedTransaction() }; + + bool visited{}; + Artist::find(session, Artist::FindParameters{}, [&](const Artist::pointer& a) + { + visited = true; + EXPECT_EQ(a->getId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } } TEST_F(DatabaseFixture, Artist_singleTrack) @@ -80,7 +92,7 @@ TEST_F(DatabaseFixture, Artist_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -215,7 +227,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index 075fdb9a..aeea9be6 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -55,20 +55,20 @@ TEST_F(DatabaseFixture, Cluster) } { - const auto clusters{ Cluster::findOrphans(session, Range{}) }; + const auto clusters{ Cluster::findOrphans(session) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), cluster.getId()); } - auto clusterTypes{ ClusterType::find(session, Range {}) }; + auto clusterTypes{ ClusterType::find(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::findUsed(session, Range{}); + clusterTypes = ClusterType::findUsed(session); ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::findOrphans(session, Range{}); + clusterTypes = ClusterType::findOrphans(session); EXPECT_TRUE(clusterTypes.results.empty()); } } @@ -76,11 +76,11 @@ TEST_F(DatabaseFixture, Cluster) { auto transaction{ session.createUniqueTransaction() }; - auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; + auto clusterTypes{ ClusterType::findOrphans(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - ASSERT_TRUE(ClusterType::findUsed(session, Range{}).results.empty()); + ASSERT_TRUE(ClusterType::findUsed(session).results.empty()); } } @@ -91,8 +91,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + auto clusterTypes{ ClusterType::findOrphans(session) }; ASSERT_EQ(clusterTypes.results.size(), 1); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); } @@ -102,7 +102,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; EXPECT_EQ(clusters.results.size(), 2); EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusterIds().empty()); @@ -127,11 +127,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrack) { auto transaction{ session.createSharedTransaction() }; - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), cluster2.getId()); - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); } { @@ -225,11 +225,11 @@ TEST_F(DatabaseFixture, Cluster_multiTracks) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size()); - for (TrackId trackId : cluster->getTracks(Range{}).results) + for (TrackId trackId : cluster->getTracks().results) { auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) }; EXPECT_TRUE(it != std::cend(tracks)); @@ -237,102 +237,6 @@ TEST_F(DatabaseFixture, Cluster_multiTracks) } } -TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) -{ - ScopedClusterType clusterType{ session, "ClusterType" }; - ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" }; - ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Cluster2" }; - ScopedCluster cluster3{ session, clusterType.lockAndGet(), "Cluster3" }; - ScopedTrack trackA{ session, "TrackA" }; - ScopedTrack trackB{ session, "TrackB" }; - ScopedTrack trackC{ session, "TrackC" }; - ScopedRelease releaseA{ session, "ReleaseA" }; - ScopedRelease releaseB{ session, "ReleaseB" }; - ScopedRelease releaseC{ session, "ReleaseC" }; - - ScopedUser user{ session, "MyUser" }; - ScopedTrackList trackList{ session, "TrackList", TrackListType::Playlist, false, user.lockAndGet() }; - - { - auto transaction{ session.createSharedTransaction() }; - - EXPECT_EQ(trackList->getDuration(), std::chrono::seconds{ 0 }); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - cluster1.get().modify()->addTrack(trackA.get()); - cluster2.get().modify()->addTrack(trackB.get()); - cluster2.get().modify()->addTrack(trackC.get()); - cluster3.get().modify()->addTrack(trackC.get()); - - trackA.get().modify()->setRelease(releaseA.get()); - trackB.get().modify()->setRelease(releaseB.get()); - trackC.get().modify()->setRelease(releaseC.get()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(trackA.get(), trackList.get()); - session.create(trackB.get(), trackList.get()); - session.create(trackB.get(), trackList.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - const auto releases{ trackList->getTopReleases({}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), releaseB.getId()); - EXPECT_EQ(releases[1]->getId(), releaseA.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - auto releases{ trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseA.getId()); - - releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseB.getId()); - - releases = trackList->getTopReleases({ cluster2.getId(), cluster1.getId() }, std::nullopt, hasMore); - EXPECT_TRUE(releases.empty()); - - releases = trackList->getTopReleases({ cluster2.getId(), cluster3.getId() }, std::nullopt, hasMore); - EXPECT_TRUE(releases.empty()); - } - - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(trackC.get(), trackList.get()); - session.create(trackC.get(), trackList.get()); - session.create(trackC.get(), trackList.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool hasMore; - auto releases{ trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), releaseC.getId()); - - releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore); - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), releaseC.getId()); - EXPECT_EQ(releases[1]->getId(), releaseB.getId()); - } -} - TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { ScopedTrack track{ session, "MyTrackFile" }; @@ -340,7 +244,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); } ScopedClusterType clusterType{ session, "MyClusterType" }; @@ -349,7 +253,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { auto transaction{ session.createSharedTransaction() }; - ASSERT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); + ASSERT_EQ(Cluster::findOrphans(session).results.size(), 2); EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty()); EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); @@ -367,7 +271,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) auto transaction{ session.createSharedTransaction() }; { - auto clusters{ Cluster::findOrphans(session, Range {}) }; + auto clusters{ Cluster::findOrphans(session) }; ASSERT_EQ(clusters.results.size(), 1); EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); } @@ -425,10 +329,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); - EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); + EXPECT_EQ(Cluster::findOrphans(session).results.size(), 2); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -486,9 +390,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -526,8 +430,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { @@ -562,7 +466,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) { auto transaction{ session.createSharedTransaction() }; - const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; + const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) }; EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); for (const TrackId similarTrackId : similarTracks.results) { @@ -610,7 +514,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) } { - auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; + auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) }; 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)); @@ -637,10 +541,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Cluster::findOrphans(session).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); } { @@ -775,259 +679,6 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters) } } -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) -{ - ScopedUser user{ session, "MyUser" }; - ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; - ScopedClusterType clusterType{ session, "MyClusterType" }; - ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; - ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" }; - ScopedCluster cluster3{ session, clusterType.lockAndGet(), "MyCluster3" }; - ScopedTrack track1{ session, "MyTrack1" }; - ScopedTrack track2{ session, "MyTrack1" }; - ScopedArtist artist1{ session, "MyArtist1" }; - ScopedArtist artist2{ session, "MyArtist2" }; - ScopedRelease release1{ session, "MyRelease1" }; - ScopedRelease release2{ session, "MyRelease2" }; - - const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; - - { - auto transaction{ session.createUniqueTransaction() }; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - - cluster1.get().modify()->addTrack(track1.get()); - cluster2.get().modify()->addTrack(track2.get()); - cluster3.get().modify()->addTrack(track1.get()); - cluster3.get().modify()->addTrack(track2.get()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track1.get(), trackList.get(), now); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty()); - - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track2.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track1.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track2.getId()); - } - - { - auto transaction{ session.createUniqueTransaction() }; - - session.create(track1.get(), trackList.get(), now.addSecs(2)); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } - - { - auto transaction{ session.createSharedTransaction() }; - - bool moreResults{}; - const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) }; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) }; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } -} - TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { ScopedArtist artist1{ session, "MyArtist1" }; diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp index 68c49382..d63ce4e4 100644 --- a/src/libs/services/database/test/Common.cpp +++ b/src/libs/services/database/test/Common.cpp @@ -42,8 +42,7 @@ TmpDatabase::TmpDatabase() { } -Database::Db& -TmpDatabase::getDb() +Database::Db& TmpDatabase::getDb() { return _db; } @@ -53,8 +52,7 @@ DatabaseFixture::~DatabaseFixture() testDatabaseEmpty(); } -void -DatabaseFixture::SetUpTestCase() +void DatabaseFixture::SetUpTestCase() { _tmpDb = std::make_unique(); { @@ -65,8 +63,8 @@ DatabaseFixture::SetUpTestCase() // remove default created entries { auto transaction{ s.createUniqueTransaction() }; - auto clusterTypes{ Database::ClusterType::find(s, Database::Range {}) }; - for (auto clusterTypeId : clusterTypes.results) + + for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results) { auto clusterType{ Database::ClusterType::find(s, clusterTypeId) }; clusterType.remove(); @@ -75,14 +73,12 @@ DatabaseFixture::SetUpTestCase() } } -void -DatabaseFixture::TearDownTestCase() +void DatabaseFixture::TearDownTestCase() { _tmpDb.reset(); } -void -DatabaseFixture::testDatabaseEmpty() +void DatabaseFixture::testDatabaseEmpty() { using namespace Database; @@ -153,10 +149,10 @@ TEST_F(DatabaseFixture, Common_subRangeForeach) { std::vector subRanges; foreachSubRange(test.range, test.subRangeSize, [&](Range subRange) - { - subRanges.push_back(subRange); - return true; - }); + { + subRanges.push_back(subRange); + return true; + }); EXPECT_EQ(subRanges, test.expectedSubRanges) << ", test index = " << std::distance(std::cbegin(testCases), &test); } diff --git a/src/libs/services/database/test/DatabaseTest.cpp b/src/libs/services/database/test/DatabaseTest.cpp index 2b35929f..bb1926dd 100644 --- a/src/libs/services/database/test/DatabaseTest.cpp +++ b/src/libs/services/database/test/DatabaseTest.cpp @@ -42,8 +42,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); } { diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index cb3a35b2..a07a6e62 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -40,7 +40,7 @@ TEST_F(DatabaseFixture, Release) EXPECT_TRUE(Release::exists(session, release.getId())); { - const auto releases{ Release::findOrphanIds(session, Range {}) }; + const auto releases{ Release::findOrphanIds(session) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -57,6 +57,16 @@ TEST_F(DatabaseFixture, Release) ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front()->getId(), release.getId()); } + + { + bool visited{}; + Release::find(session, Release::FindParameters{}, [&](const Release::pointer& r) + { + visited = true; + EXPECT_EQ(r->getId(), release.getId()); + }); + EXPECT_TRUE(visited); + } } } @@ -76,7 +86,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) { auto transaction{ session.createSharedTransaction() }; - EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; ASSERT_EQ(tracks.results.size(), 1); @@ -114,7 +124,7 @@ TEST_F(DatabaseFixture, Release_singleTrack) const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; EXPECT_TRUE(tracks.results.empty()); - auto releases{ Release::findOrphanIds(session, Range {}) }; + auto releases{ Release::findOrphanIds(session) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } diff --git a/src/libs/services/database/test/Track.cpp b/src/libs/services/database/test/Track.cpp index 0711edb6..c88e9b69 100644 --- a/src/libs/services/database/test/Track.cpp +++ b/src/libs/services/database/test/Track.cpp @@ -31,6 +31,12 @@ TEST_F(DatabaseFixture, Track) EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0); EXPECT_EQ(Track::getCount(session), 0); EXPECT_FALSE(Track::exists(session, 0)); + + { + bool visited{}; + Track::find(session, Track::FindParameters{}, [&](const Track::pointer&) {visited = true;}); + EXPECT_FALSE(visited); + } } ScopedTrack track{ session, "MyTrackFile" }; @@ -44,6 +50,16 @@ TEST_F(DatabaseFixture, Track) auto myTrack{ Track::find(session, track.getId()) }; ASSERT_TRUE(myTrack); EXPECT_EQ(myTrack->getId(), track.getId()); + + { + bool visited{}; + Track::find(session, Track::FindParameters{}, [&](const Track::pointer& t) + { + visited = true; + EXPECT_EQ(t->getId(), track.getId()); + }); + EXPECT_TRUE(visited); + } } } diff --git a/src/libs/services/database/test/TrackBookmark.cpp b/src/libs/services/database/test/TrackBookmark.cpp index d99b0bd5..d8720beb 100644 --- a/src/libs/services/database/test/TrackBookmark.cpp +++ b/src/libs/services/database/test/TrackBookmark.cpp @@ -49,7 +49,7 @@ TEST_F(DatabaseFixture, TrackBookmark) EXPECT_EQ(TrackBookmark::getCount(session), 1); - const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})}; + const auto bookmarks {TrackBookmark::find(session, user.getId())}; ASSERT_EQ(bookmarks.results.size(), 1); EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); } diff --git a/src/libs/services/database/test/TrackFeatures.cpp b/src/libs/services/database/test/TrackFeatures.cpp index 5d919a40..0f824124 100644 --- a/src/libs/services/database/test/TrackFeatures.cpp +++ b/src/libs/services/database/test/TrackFeatures.cpp @@ -41,7 +41,7 @@ TEST_F(DatabaseFixture, TrackFeatures) auto transaction {session.createUniqueTransaction()}; EXPECT_EQ(TrackFeatures::getCount(session), 1); - auto allTrackFeatures {TrackFeatures::find(session, Range {})}; + auto allTrackFeatures {TrackFeatures::find(session)}; ASSERT_EQ(allTrackFeatures.results.size(), 1); EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId()); } diff --git a/src/libs/services/database/test/TrackList.cpp b/src/libs/services/database/test/TrackList.cpp index f05cb48a..70467128 100644 --- a/src/libs/services/database/test/TrackList.cpp +++ b/src/libs/services/database/test/TrackList.cpp @@ -25,497 +25,200 @@ using namespace Database; TEST_F(DatabaseFixture, SingleTrackList) { - ScopedUser user {session, "MyUser"}; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(TrackList::getCount(session), 0); - } + ScopedUser user{ session, "MyUser" }; + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(TrackList::getCount(session), 0); + } - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; + ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() }; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(TrackList::getCount(session), 1); - } + { + auto transaction{ session.createSharedTransaction() }; + EXPECT_EQ(TrackList::getCount(session), 1); + } } TEST_F(DatabaseFixture, SingleTrackListSingleTrack) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; - EXPECT_EQ(tracks.results.size(), 0); + auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) }; + EXPECT_EQ(tracks.results.size(), 0); - tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); + EXPECT_EQ(tracks.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - session.create(track.get(), trackList1.get()); - } + session.create(track.get(), trackList1.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.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::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); + EXPECT_EQ(tracks.results.size(), 0); + } } TEST_F(DatabaseFixture, TrackList_SortMethod) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList1.getId()); - EXPECT_EQ(trackLists.results[1], trackList2.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); - trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); - } + trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} }); + trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} }); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList2.getId()); - EXPECT_EQ(trackLists.results[1], trackList1.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList2.getId()); + EXPECT_EQ(trackLists.results[1], trackList1.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); - trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); - } + trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} }); + trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} }); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; - ASSERT_EQ(trackLists.results.size(), 2); - EXPECT_EQ(trackLists.results[0], trackList1.getId()); - EXPECT_EQ(trackLists.results[1], trackList2.getId()); - } + const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) }; + ASSERT_EQ(trackLists.results.size(), 2); + EXPECT_EQ(trackLists.results[0], trackList1.getId()); + EXPECT_EQ(trackLists.results[1], trackList2.getId()); + } } TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - std::list tracks; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() }; + std::list tracks; - for (std::size_t i {}; i < 10; ++i) - { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + for (std::size_t i{}; i < 10; ++i) + { + tracks.emplace_back(session, "MyTrack" + std::to_string(i)); - auto transaction {session.createUniqueTransaction()}; - session.create(tracks.back().get(), trackList.get()); - } + auto transaction{ session.createUniqueTransaction() }; + session.create(tracks.back().get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - ASSERT_EQ(trackList->getCount(), tracks.size()); - const auto trackIds {trackList->getTrackIds()}; - ASSERT_EQ(trackIds.size(), tracks.size()); + ASSERT_EQ(trackList->getCount(), tracks.size()); + const auto trackIds{ trackList->getTrackIds() }; + ASSERT_EQ(trackIds.size(), tracks.size()); - // Same order - std::size_t i {}; - for (const ScopedTrack& track : tracks) - EXPECT_EQ(track.getId(), trackIds[i++]); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(reverseTracks.size(), tracks.size()); - - // Reverse order - ASSERT_TRUE(tracks.size() > 0); - std::size_t i {tracks.size() - 1}; - for (const ScopedTrack& track : tracks) - EXPECT_EQ(track.getId(), reverseTracks[i--]->getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedTrack track3 {session, "MyTrack3"}; - - { - Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get(), now); - session.create(track2.get(), trackList.get(), now.addSecs(-1)); - session.create(track3.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults; - const auto tracks {trackList.get()->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 3); - EXPECT_EQ(tracks.front()->getId(), track3.getId()); - EXPECT_EQ(tracks.back()->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack1"}; - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - ScopedRelease release1 {session, "MyRelease1"}; - ScopedRelease release2 {session, "MyRelease2"}; - - const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - } - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track1.get(), trackList.get(), now); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - EXPECT_EQ(tracks.size(), 1); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track2.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - session.create(track1.get(), trackList.get(), now.addSecs(2)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getArtists) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedRelease release {session, "MyRelease"}; - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(artists.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - session.create(track1.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 1); - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - EXPECT_TRUE(artists.empty()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - session.create(track2.get(), trackList.get()); - artist1.get().modify()->setSortName("ZZZ"); - artist2.get().modify()->setSortName("AAA"); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - EXPECT_EQ(artists.back()->getId(), artist2.getId()); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); - EXPECT_EQ(artists.back()->getId(), artist1.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getReleases) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track {session, "MyTrack"}; - ScopedRelease release {session, "MyRelease"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(releases.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - track.get().modify()->setRelease(release.get()); - - session.create(track.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_EQ(trackList->getCount(), 1); - bool moreResults {}; - const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(releases.size(), 1); - ASSERT_EQ(releases.front()->getId(), release->getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackList_getTracks) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack1"}; - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(trackList->getCount(), 0); - bool moreResults {}; - const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_TRUE(tracks.empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get()); - session.create(track2.get(), trackList.get()); - session.create(track1.get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_EQ(trackList->getCount(), 3); - bool moreResults {}; - const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - ASSERT_EQ(tracks[0]->getId(), track1->getId()); - ASSERT_EQ(tracks[1]->getId(), track2->getId()); - } + // Same order + std::size_t i{}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), trackIds[i++]); + } } TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedClusterType clusterType {session, "MyClusterType"}; - ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; - ScopedTrack track {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedClusterType clusterType{ session, "MyClusterType" }; + ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; + ScopedTrack track{ session, "MyTrack" }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; - EXPECT_EQ(trackLists.results.size(), 0); - } + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + EXPECT_EQ(trackLists.results.size(), 0); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - session.create(track.get(), trackList1.get()); - cluster.get().modify()->addTrack(track.get()); - } + session.create(track.get(), trackList1.get()); + cluster.get().modify()->addTrack(track.get()); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; - ASSERT_EQ(trackLists.results.size(), 1); - EXPECT_EQ(trackLists.results.front(), trackList1.getId()); - } + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + ASSERT_EQ(trackLists.results.size(), 1); + EXPECT_EQ(trackLists.results.front(), trackList1.getId()); + } } TEST_F(DatabaseFixture, SingleTrackList_getEntries) { - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack"}; - ScopedTrack track2 {session, "MyTrack"}; + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track1{ session, "MyTrack" }; + ScopedTrack track2{ session, "MyTrack" }; - { - auto transaction {session.createUniqueTransaction()}; - session.create(track1.get(), trackList.get()); - } + { + auto transaction{ session.createUniqueTransaction() }; + session.create(track1.get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries()}; - ASSERT_EQ(entries.size(), 1); - EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries() }; + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); + } - { - auto transaction {session.createUniqueTransaction()}; - session.create(track2.get(), trackList.get()); - } + { + auto transaction{ session.createUniqueTransaction() }; + session.create(track2.get(), trackList.get()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries()}; - ASSERT_EQ(entries.size(), 2); - EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); - EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries() }; + ASSERT_EQ(entries.size(), 2); + EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); + EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); + } - { - auto transaction {session.createSharedTransaction()}; - auto entries {trackList.get()->getEntries(Range {1, 1})}; - ASSERT_EQ(entries.size(), 1); - EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); - } + { + auto transaction{ session.createSharedTransaction() }; + auto entries{ trackList.get()->getEntries(Range {1, 1}) }; + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); + } } diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp index 2a9a60a9..fb66c740 100644 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -87,23 +87,23 @@ namespace Feedback return getStarredDateTime(userId, artistId); } - FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, Range range) + FeedbackService::ArtistContainer FeedbackService::findStarredArtists(const ArtistFindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Artist::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setLinkType(linkType); - params.setSortMethod(sortMethod); - params.setRange(range); + Artist::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setLinkType(params.linkType); + searchParams.setSortMethod(params.sortMethod); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Artist::findIds(session, params); + return Artist::findIds(session, searchParams); } void FeedbackService::star(UserId userId, ReleaseId releaseId) @@ -126,22 +126,22 @@ namespace Feedback return getStarredDateTime(userId, releaseId); } - FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector& clusterIds, Range range) + FeedbackService::ReleaseContainer FeedbackService::findStarredReleases(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Release::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setSortMethod(ReleaseSortMethod::StarredDateDesc); - params.setRange(range); + Release::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Release::findIds(session, params); + return Release::findIds(session, searchParams); } void FeedbackService::star(UserId userId, TrackId trackId) @@ -164,22 +164,22 @@ namespace Feedback return getStarredDateTime(userId, trackId); } - FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector& clusterIds, Range range) + FeedbackService::TrackContainer FeedbackService::findStarredTracks(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(userId) }; + auto backend{ getUserFeedbackBackend(params.user) }; if (!backend) return {}; - Track::FindParameters params; - params.setStarringUser(userId, *backend); - params.setClusters(clusterIds); - params.setSortMethod(TrackSortMethod::StarredDateDesc); - params.setRange(range); + Track::FindParameters searchParams; + searchParams.setStarringUser(params.user, *backend); + searchParams.setClusters(params.clusters); + searchParams.setSortMethod(TrackSortMethod::StarredDateDesc); + searchParams.setRange(params.range); Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - return Track::findIds(session, params); + return Track::findIds(session, searchParams); } } // ns Feedback diff --git a/src/libs/services/feedback/impl/FeedbackService.hpp b/src/libs/services/feedback/impl/FeedbackService.hpp index 279a8fa7..3b28d469 100644 --- a/src/libs/services/feedback/impl/FeedbackService.hpp +++ b/src/libs/services/feedback/impl/FeedbackService.hpp @@ -46,19 +46,19 @@ namespace Feedback void unstar(Database::UserId userId, Database::ArtistId artistId) override; bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override; - ArtistContainer getStarredArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::ArtistSortMethod sortMethod, Database::Range range) override; + ArtistContainer findStarredArtists(const ArtistFindParameters& params) override; void star(Database::UserId userId, Database::ReleaseId releaseId) override; void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override; - ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + ReleaseContainer findStarredReleases(const FindParameters& params) override; void star(Database::UserId userId, Database::TrackId trackId) override; void unstar(Database::UserId userId, Database::TrackId trackId) override; bool isStarred(Database::UserId userId, Database::TrackId trackId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override; - TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + TrackContainer findStarredTracks(const FindParameters& params) override; std::optional getUserFeedbackBackend(Database::UserId userId); diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index dbd8085f..05ed487f 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -48,23 +48,46 @@ namespace Feedback using ReleaseContainer = Database::RangeResults; using TrackContainer = Database::RangeResults; - virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual ArtistContainer getStarredArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::ArtistSortMethod sortMethod, Database::Range range) = 0; + struct FindParameters + { + Database::UserId user; + std::vector clusters; // if non empty, at least one artist that belongs to these clusters + std::optional range; + FindParameters& setUser(const Database::UserId _user) { user = _user; return *this; } + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setRange(std::optional _range) { range = _range; return *this; } + }; + + // Artists + struct ArtistFindParameters : public FindParameters + { + std::optional linkType; // if set, only artists that have produced at least one track with this link type + Database::ArtistSortMethod sortMethod{ Database::ArtistSortMethod::None }; + + ArtistFindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } + ArtistFindParameters& setSortMethod(Database::ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } + }; + + virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0; + + // Releases virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0; - virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0; + // Tracks virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0; - virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual TrackContainer findStarredTracks(const FindParameters& params) = 0; }; std::unique_ptr createFeedbackService(boost::asio::io_service& ioService, Database::Db& db); diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp index 1cfb6811..60113e0b 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp @@ -120,7 +120,7 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr auto transaction {session.createSharedTransaction()}; LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features..."; - trackFeaturesIds = TrackFeatures::find(session, Range {}); + trackFeaturesIds = TrackFeatures::find(session); LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)"; } diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.cpp index 0e18f4a5..e1893a8a 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::findIdsTrackMBIDDuplicates(session, Range {}); + const RangeResults tracks = Track::findIdsTrackMBIDDuplicates(session); for (const TrackId trackId : tracks.results) { const Track::pointer track {Track::find(session, trackId)}; diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp index a9572fac..1ae6781d 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.cpp @@ -30,174 +30,169 @@ namespace Scanner { - void - ScanStepRemoveOrphanDbFiles::process(ScanContext& context) - { - removeOrphanTracks(context); - removeOrphanClusters(); - removeOrphanArtists(); - removeOrphanReleases(); - } + void ScanStepRemoveOrphanDbFiles::process(ScanContext& context) + { + removeOrphanTracks(context); + removeOrphanClusters(); + removeOrphanArtists(); + removeOrphanReleases(); + } - void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context) - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context) + { + using namespace Database; - if (_abortScan) - return; + if (_abortScan) + return; - static constexpr std::size_t batchSize {50}; - Session& session {_db.getTLSSession()}; + static constexpr std::size_t batchSize{ 50 }; + Session& session{ _db.getTLSSession() }; - LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; - std::size_t trackCount {}; + LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; + std::size_t trackCount{}; - { - auto transaction {session.createSharedTransaction()}; - trackCount = Track::getCount(session); - } - LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; + { + auto transaction{ session.createSharedTransaction() }; + trackCount = Track::getCount(session); + } + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; - context.currentStepStats.totalElems = trackCount; + context.currentStepStats.totalElems = trackCount; - RangeResults trackPaths; - std::vector tracksToRemove; + RangeResults trackPaths; + std::vector tracksToRemove; - // TODO handle only files in context.directory - for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) - { - tracksToRemove.clear(); + // TODO handle only files in context.directory + for (std::size_t i{ trackCount < batchSize ? 0 : trackCount - batchSize }; ; i -= (i > batchSize ? batchSize : i)) + { + tracksToRemove.clear(); - { - auto transaction {session.createSharedTransaction()}; - trackPaths = Track::findPaths(session, Range {i, batchSize}); - } + { + auto transaction{ session.createSharedTransaction() }; + trackPaths = Track::findPaths(session, Range{ i, batchSize }); + } - for (const Track::PathResult& trackPath : trackPaths.results) - { - if (_abortScan) - return; + for (const Track::PathResult& trackPath : trackPaths.results) + { + if (_abortScan) + return; - if (!checkFile(trackPath.path)) - tracksToRemove.push_back(trackPath.trackId); + if (!checkFile(trackPath.path)) + tracksToRemove.push_back(trackPath.trackId); - context.currentStepStats.processedElems++; - } + context.currentStepStats.processedElems++; + } - if (!tracksToRemove.empty()) - { - auto transaction {session.createSharedTransaction()}; + if (!tracksToRemove.empty()) + { + auto transaction{ session.createSharedTransaction() }; - for (const TrackId trackId : tracksToRemove) - { - Track::pointer track {Track::find(session, trackId)}; - if (track) - { - track.remove(); - context.stats.deletions++; - } - } - } + for (const TrackId trackId : tracksToRemove) + { + Track::pointer track{ Track::find(session, trackId) }; + if (track) + { + track.remove(); + context.stats.deletions++; + } + } + } - _progressCallback(context.currentStepStats); + _progressCallback(context.currentStepStats); - if (i == 0) - break; - } + if (i == 0) + break; + } - LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; - } + LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanClusters() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanClusters() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - // Now process orphan Cluster (no track) - auto clusterIds {Cluster::findOrphans(session, Range {})}; - for (ClusterId clusterId : clusterIds.results) - { - Cluster::pointer cluster {Cluster::find(session, clusterId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; - cluster.remove(); - } - } + // Now process orphan Cluster (no track) + auto clusterIds{ Cluster::findOrphans(session) }; + for (ClusterId clusterId : clusterIds.results) + { + Cluster::pointer cluster{ Cluster::find(session, clusterId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; + cluster.remove(); + } + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanArtists() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanArtists() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists..."; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists..."; - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - auto artistIds {Artist::findOrphanIds(session, Range {})}; - for (const ArtistId artistId : artistIds.results) - { - Artist::pointer artist {Artist::find(session, artistId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; - artist.remove(); - } - } + auto artistIds{ Artist::findOrphanIds(session) }; + for (const ArtistId artistId : artistIds.results) + { + Artist::pointer artist{ Artist::find(session, artistId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; + artist.remove(); + } + } - void - ScanStepRemoveOrphanDbFiles::removeOrphanReleases() - { - using namespace Database; + void ScanStepRemoveOrphanDbFiles::removeOrphanReleases() + { + using namespace Database; - LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; + LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; - // TODO, by batch - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; + // TODO, by batch + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; - auto releases {Release::findOrphanIds(session, Range {})}; - for (const ReleaseId releaseId : releases.results) - { - Release::pointer release {Release::find(session, releaseId)}; - LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; - release.remove(); - } - } + auto releases{ Release::findOrphanIds(session) }; + for (const ReleaseId releaseId : releases.results) + { + Release::pointer release{ Release::find(session, releaseId) }; + LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; + release.remove(); + } + } - bool - ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p) - { - try - { - // For each track, make sure the the file still exists - // and still belongs to a media directory - if (!std::filesystem::exists( p ) - || !std::filesystem::is_regular_file( p ) ) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing"; - return false; - } + bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p) + { + try + { + // For each track, make sure the the file still exists + // and still belongs to a media directory + if (!std::filesystem::exists(p) + || !std::filesystem::is_regular_file(p)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing"; + return false; + } - if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName)) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory"; - return false; - } + if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory"; + return false; + } - if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions)) - { - LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled"; - return false; - } + if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions)) + { + LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled"; + return false; + } - return true; - } - catch (std::filesystem::filesystem_error& e) - { - LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what(); - return false; - } - } + return true; + } + catch (std::filesystem::filesystem_error& e) + { + LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what(); + return false; + } + } } diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 18cef189..60690dae 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -29,6 +29,8 @@ namespace API::Subsonic { + // Max count expected from all API methods that expose a count + static inline constexpr std::size_t defaultMaxCountSize{ 1000 }; enum class ResponseFormat { diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 5dc3b2b4..6a55ffc6 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -47,8 +47,8 @@ namespace API::Subsonic // Optional params const std::size_t size{ getParameterAs(context.parameters, "size").value_or(10) }; const std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; - if (size > 500) - throw ParameterValueTooHighGenericError{ "size", 500 }; + if (size > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; const Range range{ offset, size }; @@ -122,7 +122,7 @@ namespace API::Subsonic // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) Release::FindParameters params; params.setSortMethod(ReleaseSortMethod::Random); - params.setRange({ 0, size }); + params.setRange(Range{ 0, size }); releases = Release::findIds(context.dbSession, params); } @@ -132,10 +132,15 @@ namespace API::Subsonic } else if (type == "starred") { - releases = feedbackService.getStarredReleases(context.userId, {}, range); + Feedback::IFeedbackService::FindParameters params; + params.setUser(context.userId); + params.setRange(range); + releases = feedbackService.findStarredReleases(params); } else + { throw NotImplementedGenericError{}; + } Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) }; @@ -162,19 +167,27 @@ namespace API::Subsonic Feedback::IFeedbackService& feedbackService{ *Service::get() }; - for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) + Feedback::IFeedbackService::FindParameters findParameters; + findParameters.setUser(context.userId); + { - if (auto artist{ Artist::find(context.dbSession, artistId) }) - starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + Feedback::IFeedbackService::ArtistFindParameters artistFindParams; + artistFindParams.setUser(context.userId); + artistFindParams.setSortMethod(ArtistSortMethod::BySortName); + for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) + { + if (auto artist{ Artist::find(context.dbSession, artistId) }) + starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + } } - for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) + for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results) { if (auto release{ Release::find(context.dbSession, releaseId) }) starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); } - for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) + for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results) { if (auto track{ Track::find(context.dbSession, trackId) }) starredNode.addArrayChild("song", createSongNode(context, track, user)); @@ -198,8 +211,8 @@ namespace API::Subsonic { // Optional params std::size_t size{ getParameterAs(context.parameters, "size").value_or(50) }; - if (size > 500) - throw ParameterValueTooHighGenericError{"size", 500}; + if (size > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; auto transaction{ context.dbSession.createSharedTransaction() }; @@ -207,13 +220,17 @@ namespace API::Subsonic if (!user) throw UserNotAuthorizedError{}; - 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 Track::pointer& track : tracks.results) - randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); + + Track::FindParameters params; + params.setSortMethod(TrackSortMethod::Random); + params.setRange(Range{ 0, size }); + + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); + }); return response; } @@ -225,8 +242,8 @@ namespace API::Subsonic // Optional params std::size_t count{ getParameterAs(context.parameters, "count").value_or(10) }; - if (count > 500) - throw ParameterValueTooHighGenericError{"count", 500}; + if (count > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{"count", defaultMaxCountSize}; std::size_t offset{ getParameterAs(context.parameters, "offset").value_or(0) }; @@ -249,11 +266,12 @@ namespace API::Subsonic Track::FindParameters params; params.setClusters({ cluster->getId() }); - params.setRange({ offset, count }); + params.setRange(Range{ offset, count }); - const auto tracks{ Track::find(context.dbSession, params) }; - for (const Track::pointer& track : tracks.results) - songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); + }); return response; } diff --git a/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp index a149555c..1d20eb76 100644 --- a/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp +++ b/src/libs/subsonic/impl/entrypoints/Bookmarks.cpp @@ -40,7 +40,7 @@ namespace API::Subsonic if (!user) throw UserNotAuthorizedError{}; - const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId(), Range {}) }; + const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId()) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& bookmarksNode{ response.createNode("bookmarks") }; diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 16b8ed8d..c9dcd7e0 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -124,7 +124,7 @@ namespace API::Subsonic // first pass: dispatch the artists by first letter LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists..."; std::map> artistsSortedByFirstChar; - std::size_t currentArtistOffset{0}; + std::size_t currentArtistOffset{ 0 }; constexpr std::size_t batchSize{ 100 }; bool hasMoreArtists{ true }; while (hasMoreArtists) @@ -190,7 +190,7 @@ namespace API::Subsonic { Track::FindParameters params; params.setArtist(id); - params.setRange({ 0, meanTrackCountPerArtist }); + params.setRange(Range{ 0, meanTrackCountPerArtist }); params.setSortMethod(TrackSortMethod::Random); const auto artistTracks{ Track::findIds(context.dbSession, params) }; @@ -221,7 +221,7 @@ namespace API::Subsonic { Track::FindParameters params; params.setRelease(id); - params.setRange({ 0, meanTrackCountPerRelease }); + params.setRange(Range{ 0, meanTrackCountPerRelease }); params.setSortMethod(TrackSortMethod::Random); const auto releaseTracks{ Track::findIds(context.dbSession, params) }; @@ -242,6 +242,8 @@ namespace API::Subsonic { // Optional params std::size_t count{ getParameterAs(context.parameters, "count").value_or(50) }; + if (count > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize }; std::vector tracks; @@ -316,9 +318,11 @@ namespace API::Subsonic directoryNode.setAttribute("id", idToString(RootId{})); directoryNode.setAttribute("name", "Music"); - const auto rootArtistIds{ Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; - for (const Artist::pointer& artist : rootArtistIds.results) - directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); + // TODO: this does not scale when a lot of artists are present + Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::BySortName), [&](const Artist::pointer& artist) + { + directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); + }); } else if (artistId) { @@ -330,9 +334,10 @@ namespace API::Subsonic directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName())); - const auto artistReleases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId)) }; - for (const Release::pointer& release : artistReleases.results) - directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */)); + Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release) + { + directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */)); + }); } else if (releaseId) { @@ -344,9 +349,10 @@ 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 Track::pointer& track : tracks.results) - directoryNode.addArrayChild("child", createSongNode(context, track, user)); + Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track) + { + directoryNode.addArrayChild("child", createSongNode(context, track, user)); + }); } else throw BadParameterGenericError{ "id" }; diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index 79dccac7..41d1e774 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -57,12 +57,12 @@ namespace API::Subsonic std::size_t songCount{ getParameterAs(context.parameters, "songCount").value_or(20) }; std::size_t songOffset{ getParameterAs(context.parameters, "songOffset").value_or(0) }; - if (artistCount > 500) - throw ParameterValueTooHighGenericError{ "artistCount", 500 }; - else if (albumCount > 500) - throw ParameterValueTooHighGenericError{ "albumCount", 500 }; - else if (songCount > 500) - throw ParameterValueTooHighGenericError{ "songCount", 500 }; + if (artistCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize }; + else if (albumCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize }; + else if (songCount > defaultMaxCountSize) + throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize }; Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; @@ -77,30 +77,36 @@ namespace API::Subsonic { Artist::FindParameters params; params.setKeywords(keywords); - params.setRange({ artistOffset, artistCount }); + params.setRange(Range{ artistOffset, artistCount }); - for (const Artist::pointer& artist : Artist::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) + { + searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); + }); } if (albumCount > 0) { Release::FindParameters params; params.setKeywords(keywords); - params.setRange({ albumOffset, albumCount }); + params.setRange(Range{ albumOffset, albumCount }); - for (const Release::pointer& release : Release::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); + Release::find(context.dbSession, params, [&](const Release::pointer& release) + { + searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); + }); } if (songCount > 0) { Track::FindParameters params; params.setKeywords(keywords); - params.setRange({ songOffset, songCount }); + params.setRange(Range{ songOffset, songCount }); - for (const Track::pointer& track : Track::find(context.dbSession, params).results) - searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + Track::find(context.dbSession, params, [&](const Track::pointer& track) + { + searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); + }); } return response; diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index fab4cc70..bb414679 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -799,8 +799,9 @@ namespace UserInterface params.setDistinct(false); params.setSortMethod(TrackSortMethod::TrackList); - const auto tracks{ Track::find(session, params) }; - for (const Track::pointer& track : tracks.results) - session.create(track, trackList); + Track::find(session, params, [&](const Track::pointer& track) + { + session.create(track, trackList); + }); } } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 67d08238..3f5ca7b5 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -33,16 +33,14 @@ namespace UserInterface { using namespace Database; - RangeResults ArtistCollector::get(Database::Range range) + RangeResults ArtistCollector::get(std::optional requestedRange) { Feedback::IFeedbackService& feedbackService{ *Service::get() }; Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; - RangeResults artists; + const Range range{ getActualRange(requestedRange) }; - range = getActualRange(range); - if (range.size == 0) - return artists; + RangeResults artists; switch (getMode()) { @@ -51,8 +49,16 @@ namespace UserInterface break; case Mode::Starred: - artists = feedbackService.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); + { + Feedback::IFeedbackService::ArtistFindParameters params; + params.setUser(LmsApp->getUserId()); + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::StarredDateDesc); + params.setRange(range); + artists = feedbackService.findStarredArtists(params); break; + } case Mode::RecentlyPlayed: artists = scrobblingService.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index ab9d43fc..2c918ed7 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -37,7 +37,7 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - Database::RangeResults get(Database::Range range); + Database::RangeResults get(std::optional range = std::nullopt); void reset() { _randomArtists.reset(); } void setArtistLinkType(std::optional linkType) { _linkType = linkType; } diff --git a/src/lms/ui/explore/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp index 16afcc7f..fcb80343 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.cpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp @@ -30,16 +30,25 @@ namespace UserInterface { } - DatabaseCollectorBase::Range DatabaseCollectorBase::getActualRange(Range range) const + DatabaseCollectorBase::Range DatabaseCollectorBase::getActualRange(std::optional requestedRange) const { - assert(range.size > 0); + Database::Range res; - if (range.offset < _maxCount) - range.size = std::min(_maxCount - range.offset, range.size); + if (!requestedRange) + { + res.offset = 0; + res.size = _maxCount; + } else - range.size = 0; + { + res.offset = requestedRange->offset; + if (requestedRange->offset < _maxCount) + res.size = std::min(_maxCount - requestedRange->offset, requestedRange->size); + else + res.size = 0; + } - return range; + return res; } std::size_t DatabaseCollectorBase::getMaxCount() const diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index 2d654779..52f3a458 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -56,9 +56,9 @@ namespace UserInterface void setSearch(std::string_view search); protected: - Range getActualRange(Range range) const; + Range getActualRange(std::optional range) const; std::size_t getMaxCount() const; - Filters& getFilters() { return _filters; } + Filters& getFilters() { return _filters; } const std::vector& getSearchKeywords() const { return _searchKeywords; } private: diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index a3a08487..e6b45457 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -84,7 +84,7 @@ Filters::showDialog() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession(), Range {})}; + const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession())}; for (const ClusterTypeId clusterTypeId : clusterTypesIds.results) { const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)}; diff --git a/src/lms/ui/explore/PlayQueueController.cpp b/src/lms/ui/explore/PlayQueueController.cpp index 0329b880..74fc6911 100644 --- a/src/lms/ui/explore/PlayQueueController.cpp +++ b/src/lms/ui/explore/PlayQueueController.cpp @@ -45,7 +45,7 @@ namespace UserInterface params.setArtist(artistId); params.setSortMethod(Database::TrackSortMethod::DateDescAndRelease); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount - res.size() }); + params.setRange(Database::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ Database::Track::findIds(session, params) }; @@ -74,7 +74,7 @@ namespace UserInterface params.setRelease(releaseId); params.setSortMethod(Database::TrackSortMethod::Release); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount - res.size() }); + params.setRange(Database::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ Database::Track::findIds(session, params) }; @@ -98,7 +98,7 @@ namespace UserInterface Database::Track::FindParameters params; params.setTrackList(trackListId); params.setClusters(clusters); - params.setRange({ 0, maxTrackCount }); + params.setRange(Database::Range{ 0, maxTrackCount }); params.setSortMethod(TrackSortMethod::TrackList); params.setDistinct(false); diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 4a421d4c..c3ae1e15 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -32,15 +32,14 @@ namespace UserInterface { using namespace Database; - RangeResults ReleaseCollector::get(Database::Range range) + RangeResults ReleaseCollector::get(std::optional requestedRange) { Feedback::IFeedbackService& feedbackService{ *Service::get() }; Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; + const Range range{ getActualRange(requestedRange) }; + RangeResults releases; - range = getActualRange(range); - if (range.size == 0) - return releases; switch (getMode()) { @@ -49,8 +48,14 @@ namespace UserInterface break; case Mode::Starred: - releases = feedbackService.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); + { + Feedback::IFeedbackService::FindParameters params; + params.setUser(LmsApp->getUserId()); + params.setClusters(getFilters().getClusterIds()); + params.setRange(range); + releases = feedbackService.findStarredReleases(params); break; + } case ReleaseCollector::Mode::RecentlyPlayed: releases = scrobblingService.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); @@ -118,7 +123,7 @@ namespace UserInterface Release::FindParameters params; params.setClusters(getFilters().getClusterIds()); params.setSortMethod(ReleaseSortMethod::Random); - params.setRange({ 0, getMaxCount() }); + params.setRange(Range{ 0, getMaxCount() }); { auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp index 924aabe6..1a10c205 100644 --- a/src/lms/ui/explore/ReleaseCollector.hpp +++ b/src/lms/ui/explore/ReleaseCollector.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include #include "DatabaseCollectorBase.hpp" @@ -39,7 +39,7 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - Database::RangeResults get(Database::Range range); + Database::RangeResults get(std::optional range = std::nullopt); void reset() { _randomReleases.reset(); } private: diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 40754fc2..042e9c3c 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -373,100 +373,100 @@ namespace UserInterface params.setSortMethod(Database::TrackSortMethod::Release); params.setClusters(_filters.getClusterIds()); - for (const Database::Track::pointer& track : Database::Track::find(LmsApp->getDbSession(), params).results) - { - const Database::TrackId trackId{ track->getId() }; - const auto discNumber{ track->getDiscNumber() }; - - Wt::WContainerWidget* container; - if (isReleaseMultiDisc && discNumber) - container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle()); - else - container = getOrAddNoDiscContainer(); - - Template* entry{ container->addNew