diff --git a/CMakeLists.txt b/CMakeLists.txt index 48432336..d2e90867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12) -project(lms VERSION 3.78.0) +project(lms VERSION 3.79.0) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/) diff --git a/README.md b/README.md index 16e4499a..7cda5d98 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat * [ListenBrainz](https://listenbrainz.org) support for: * Scrobbling and synchronizing listens * Synchronizing 'love' feedbacks +* [Last.fm](https://www.last.fm) support for scrobbling listens * Recommendation engine * Multi-library support * ReplayGain support @@ -41,8 +42,8 @@ _LMS_ provides several ways to help you find the music you like: * ... __Note__: the recommendation engine supports two modes: +* **Audio similarity** (default): uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 to ~25k tracks/hour on an Intel Core i5-13500. * **Tag-based**: uses metadata tags `genre`, `mood`, `grouping`, and `language`. -* **Audio similarity**: uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 (3 threads) to ~25k tracks/hour on an Intel Core i5-13500 (10 threads). To speed up extraction, consider tweaking `scanner-thread-count` in `lms.conf` (defaults to half the number of logical CPUs). ## About tags _LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md). diff --git a/SUBSONIC.md b/SUBSONIC.md index 7fd8007f..49dbbae3 100644 --- a/SUBSONIC.md +++ b/SUBSONIC.md @@ -17,13 +17,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l ## Authentication _LMS_ supports the [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/) method. Each user has to generate their own API key on the settings page to use the Subsonic API. - -By default, API keys can also be used as passwords, provided the `user` parameter matches the API key owner. To disable this fallback authentication method, set the following in `lms.conf`: -``` -api-subsonic-support-user-password-auth = false; -``` - -__Note__: the token+salt authentication method is not supported; use the API key as the password instead (see above). +If a client's login screen has no dedicated API key field, enter the API key as the password instead. ## Extra fields The following extra fields are implemented: @@ -33,6 +27,7 @@ The following extra fields are implemented: * `displayArtist` * `explicitStatus` * `genres` + * `groupings` * `isCompilation` * `played` * `mediaType` @@ -56,12 +51,15 @@ The following extra fields are implemented: * `displayArtist` * `explicitStatus` * `genres` + * `groupings` * `mediaType` * `moods` + * `movements` * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `played` * `replayGain` * `samplingRate` + * `works` * `Artist` response: * `mediaType` * `musicBrainzId` diff --git a/approot/messages.xml b/approot/messages.xml index 146bfdbe..81819ddc 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -14,6 +14,8 @@ Discard Edit This field must be in upper case +Display limit reached — refine your search to see more results +No results found Loading... Login Logout diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 21ba6ad6..53be4490 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -14,6 +14,8 @@ Descartar Editar Este campo debe estar en mayúsculas +Límite de visualización alcanzado — refine su búsqueda para ver más resultados +No se han encontrado resultados Cargando... Usuario Salir diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 44a7f588..69751f6c 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -14,6 +14,8 @@ Annuler Editer Ce champ doit être en majuscule +Limite d'affichage atteinte — affinez votre recherche pour voir plus de résultats +Aucun résultat Chargement... Login Quitter diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 1ec58f23..3ea55b12 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -14,6 +14,8 @@ Annulla Modifica Questo campo deve essere in maiuscolo +Limite di visualizzazione raggiunto — affina la ricerca per vedere più risultati +Nessun risultato trovato Caricamento... Login Logout diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index f35671cd..1b4613f1 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -14,6 +14,8 @@ Odrzuć Zmień Wartość musi się zaczynać wielką literą +Osiągnięto limit wyświetlania — doprecyzuj wyszukiwanie, aby zobaczyć więcej wyników +Brak wyników Wczytywanie... Login Wyloguj diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index c0bc9f44..44cdfa56 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -14,6 +14,8 @@ 撤销 编辑 此字段必须为大写 +已达到显示上限 — 请精简搜索条件以查看更多结果 +未找到结果 加载中... 登陆 注销 diff --git a/approot/misc.xml b/approot/misc.xml index b62f3215..7a87c19a 100644 --- a/approot/misc.xml +++ b/approot/misc.xml @@ -9,6 +9,7 @@ + ${elements} ${loading-indicator} diff --git a/approot/playqueue.xml b/approot/playqueue.xml index 2f6f2453..468c1c72 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -40,18 +40,19 @@
${cover}
-
+
-
-
${name}
- ${}${artists class="d-block d-md-none text-truncate"}${} +
+
${name}
+ ${}${artists class="d-block d-md-none"}${} + ${}
${work}
${
}
- ${}${artists-md class="d-none d-md-block text-truncate"}${} + ${}${artists-md class="d-none d-md-block"}${}
-
- ${}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${} +
+ ${}${release class="d-none d-lg-inline text-decoration-none link-success"}${}
diff --git a/approot/release.xml b/approot/release.xml index a542b941..fdcf8568 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -55,36 +55,43 @@ ${artist class="text-decoration-none link-success"} - -

- ${} -
${artwork class="Lms-cursor-pointer"}
- ${
} -
${disc-title}
-
- ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} - + + ${} +
${artwork class="Lms-cursor-pointer"}
+ ${
} +
${title}
+
+ ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} + -

+
+ + + +

${header class="d-flex align-items-center"}

+ ${tracks class="d-grid gap-1"} +
+ + +
${header class="d-flex align-items-center"}
${tracks class="d-grid gap-1 Lms-row-container"}
- + ${tracks class="d-grid gap-1 Lms-row-container"} - +
- ${}${track-number}${} + ${}${position}${}
diff --git a/approot/settings-subsonic.xml b/approot/settings-subsonic.xml index ab36a43f..d22bd7b8 100644 --- a/approot/settings-subsonic.xml +++ b/approot/settings-subsonic.xml @@ -54,11 +54,9 @@
${tr:Lms.Settings.subsonic-token} - ${} - ${}
diff --git a/approot/tracks.xml b/approot/tracks.xml index 76dba9ae..b931dcae 100644 --- a/approot/tracks.xml +++ b/approot/tracks.xml @@ -32,18 +32,19 @@
${cover}
-
+
-
-
${name}
- ${}${artists class="d-block d-md-none text-truncate"}${} +
+
${name}
+ ${}${artists class="d-block d-md-none"}${} + ${}
${work}
${
}
- ${}${artists-md class="d-none d-md-block text-truncate"}${} + ${}${artists-md class="d-none d-md-block"}${}
-
- ${}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${} +
+ ${}${release class="d-none d-lg-inline text-decoration-none link-success"}${}
diff --git a/conf/lms.conf b/conf/lms.conf index e53559b8..7231c04b 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -84,14 +84,6 @@ login-throttler-max-entries = 10000; # API api-subsonic = true; -# Enable or disable user/password authentication for the Subsonic API. -# Note: Since token/salt authentication is always disabled, setting this to 'false' means only API keys can be used to access the Subsonic API. -api-subsonic-support-user-password-auth = 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-old-server-protocol-clients = ("DSub"); - # List of clients for whom open subsonic extensions and extra fields are disabled api-open-subsonic-disabled-clients = ("DSub"); diff --git a/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp b/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp index b77290f9..97612035 100644 --- a/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp +++ b/src/libs/audio/impl/ffmpeg/PcmDecoder.cpp @@ -65,7 +65,7 @@ namespace lms::audio::ffmpeg return planar ? AV_SAMPLE_FMT_DBLP : AV_SAMPLE_FMT_DBL; } - throw Exception("Unsupported PcmSampleType"); + throw Exception{ "Unsupported PcmSampleType" }; } SwrContextPtr createResampler(const PcmParameters& params, const AVChannelLayout& inLayout, AVSampleFormat inFmt, int inSampleRate) @@ -104,7 +104,7 @@ namespace lms::audio::ffmpeg : _parameters{ parameters } { if (_parameters.channelCount > AV_NUM_DATA_POINTERS) - throw Exception("Channel count exceeds maximum supported channels"); + throw Exception{ "Channel count exceeds maximum supported channels" }; utils::init(); @@ -179,7 +179,7 @@ namespace lms::audio::ffmpeg { int error{ ::avcodec_open2(_decoderContext.get(), decoder, nullptr) }; if (error < 0) - throw FFmpegException("Cannot open decoder", error); + throw FFmpegException{ "Cannot open decoder", error }; } _decodedFrame = AVFramePtr{ av_frame_alloc() }; diff --git a/src/libs/audio/impl/ffmpeg/TagReader.cpp b/src/libs/audio/impl/ffmpeg/TagReader.cpp index dcf648aa..97095f7e 100644 --- a/src/libs/audio/impl/ffmpeg/TagReader.cpp +++ b/src/libs/audio/impl/ffmpeg/TagReader.cpp @@ -83,9 +83,9 @@ namespace lms::audio::ffmpeg { TagType::Mixers, { "MIXERS" } }, { TagType::MixersSortOrder, { "MIXERSSORT" } }, { TagType::Mood, { "MOOD" } }, - { TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } }, - { TagType::MovementCount, { "MOVEMENTCOUNT" } }, - { TagType::MovementNumber, { "MOVEMENTNUMBER" } }, + { TagType::Movement, { "MOVEMENTNAME" } }, + { TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } }, + { TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } }, { TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } }, { TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } }, { TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } }, diff --git a/src/libs/audio/impl/pulseaudio/AudioOutput.cpp b/src/libs/audio/impl/pulseaudio/AudioOutput.cpp index 65b3cdb2..5c102558 100644 --- a/src/libs/audio/impl/pulseaudio/AudioOutput.cpp +++ b/src/libs/audio/impl/pulseaudio/AudioOutput.cpp @@ -89,13 +89,13 @@ namespace lms::audio::pulseaudio { const int error{ ::pa_context_connect(_context.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) }; if (error < 0) - throw PaException("pa_context_connect failed", error); + throw PaException{ "pa_context_connect failed", error }; } { const int error{ ::pa_threaded_mainloop_start(_mainLoop.get()) }; if (error < 0) - throw PaException("pa_threaded_mainloop_start failed", error); + throw PaException{ "pa_threaded_mainloop_start failed", error }; } } diff --git a/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp b/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp index 27832fbb..b8f357f4 100644 --- a/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp +++ b/src/libs/audio/impl/pulseaudio/AudioOutputStream.cpp @@ -226,7 +226,7 @@ namespace lms::audio::pulseaudio { pa_operation* op{ ::pa_stream_flush(_stream.get(), nullptr, nullptr) }; if (!op) - throw PaException("pa_stream_flush failed", pa_context_errno(_context)); + throw PaException{ "pa_stream_flush failed", pa_context_errno(_context) }; ::pa_operation_unref(op); } @@ -249,7 +249,7 @@ namespace lms::audio::pulseaudio pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) }; if (!op) - throw PaException("pa_stream_cork (pause) failed", pa_context_errno(_context)); + throw PaException{ "pa_stream_cork (pause) failed", pa_context_errno(_context) }; ::pa_operation_unref(op); } @@ -263,14 +263,14 @@ namespace lms::audio::pulseaudio { pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) }; if (!op) - throw PaException("pa_stream_cork (resume) failed", pa_context_errno(_context)); + throw PaException{ "pa_stream_cork (resume) failed", pa_context_errno(_context) }; ::pa_operation_unref(op); } { pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) }; if (!op) - throw PaException("pa_stream_trigger failed", pa_context_errno(_context)); + throw PaException{ "pa_stream_trigger failed", pa_context_errno(_context) }; ::pa_operation_unref(op); } } @@ -301,7 +301,7 @@ namespace lms::audio::pulseaudio nullptr, nullptr) }; if (!op) - throw PaException("pa_context_set_sink_input_volume failed", pa_context_errno(_context)); + throw PaException{ "pa_context_set_sink_input_volume failed", pa_context_errno(_context) }; ::pa_operation_unref(op); @@ -425,7 +425,7 @@ namespace lms::audio::pulseaudio ::pa_operation* op{ ::pa_stream_drain(_stream.get(), [](pa_stream*, int success, void* userdata) { static_cast(userdata)->onDrainComplete(success); }, this) }; if (!op) - throw PaException("pa_stream_drain failed", pa_context_errno(_context)); + throw PaException{ "pa_stream_drain failed", pa_context_errno(_context) }; ::pa_operation_unref(op); } diff --git a/src/libs/audio/impl/taglib/TagReader.cpp b/src/libs/audio/impl/taglib/TagReader.cpp index a905f7b5..cd3f594b 100644 --- a/src/libs/audio/impl/taglib/TagReader.cpp +++ b/src/libs/audio/impl/taglib/TagReader.cpp @@ -113,9 +113,9 @@ namespace lms::audio::taglib { TagType::Mixers, { "MIXERS" } }, { TagType::MixersSortOrder, { "MIXERSSORT" } }, { TagType::Mood, { "MOOD" } }, - { TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } }, - { TagType::MovementCount, { "MOVEMENTCOUNT" } }, - { TagType::MovementNumber, { "MOVEMENTNUMBER" } }, + { TagType::Movement, { "MOVEMENTNAME" } }, + { TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } }, + { TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } }, { TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } }, { TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } }, { TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } }, diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index 65c8826d..5b089608 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -13,6 +13,7 @@ configure_file( add_library(lmscore STATIC impl/http/Client.cpp impl/http/SendQueue.cpp + impl/http/UrlValidation.cpp impl/media/Codec.cpp impl/media/Container.cpp impl/media/ImageType.cpp diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 8fec0efb..a74e8bfe 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -341,6 +341,15 @@ namespace lms::core::stringUtils return str.substr(0, str.find_last_not_of(whitespaces) + 1); } + std::string_view utf8Truncate(std::string_view str, std::size_t maxBytes) + { + std::size_t len{ std::min(maxBytes, str.size()) }; + while (len > 0 && len < str.size() && (static_cast(str[len]) & 0xC0) == 0x80) + --len; + + return str.substr(0, len); + } + std::string stringToLower(std::string_view str) { std::string res; @@ -366,18 +375,6 @@ namespace lms::core::stringUtils return res; } - std::string bufferToString(std::span data) - { - std::ostringstream oss; - - for (unsigned char c : data) - { - oss << std::setw(2) << std::setfill('0') << std::hex << (int)c; - } - - return oss.str(); - } - bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB) { if (strA.size() != strB.size()) @@ -418,6 +415,44 @@ namespace lms::core::stringUtils } } + std::string toRomanNumeral(std::size_t n) + { + if (n == 0 || n > 3999) + return {}; + + static constexpr struct + { + std::size_t val; + const char* sym; + } table[]{ + { 1000, "m" }, + { 900, "cm" }, + { 500, "d" }, + { 400, "cd" }, + { 100, "c" }, + { 90, "xc" }, + { 50, "l" }, + { 40, "xl" }, + { 10, "x" }, + { 9, "ix" }, + { 5, "v" }, + { 4, "iv" }, + { 1, "i" } + }; + + std::string res; + for (const auto& [val, sym] : table) + { + while (n >= val) + { + res += sym; + n -= val; + } + } + + return res; + } + std::string replaceInString(std::string_view str, std::string_view from, std::string_view to) { std::string res{ str }; @@ -561,16 +596,18 @@ namespace lms::core::stringUtils return res; } - std::string toHexString(std::string_view str) + std::string bufferToHexString(std::span data) { constexpr char lut[]{ "0123456789ABCDEF" }; std::string res; + res.reserve(data.size() * 2); - for (char c : str) + for (const std::byte b : data) { - res.push_back(lut[(c >> 4) & 0xF]); - res.push_back(lut[c & 0xF]); + const unsigned value{ std::to_integer(b) }; + res.push_back(lut[(value >> 4) & 0xF]); + res.push_back(lut[value & 0xF]); } return res; diff --git a/src/libs/core/impl/http/SendQueue.cpp b/src/libs/core/impl/http/SendQueue.cpp index e4c819ec..33b2eac3 100644 --- a/src/libs/core/impl/http/SendQueue.cpp +++ b/src/libs/core/impl/http/SendQueue.cpp @@ -31,6 +31,7 @@ #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/String.hpp" +#include "core/http/UrlValidation.hpp" #define LOG(sev, message) LMS_LOG(HTTP, sev, "[Http SendQueue] - " << message) @@ -190,6 +191,12 @@ namespace lms::core::http const std::string url{ _baseUrl + request.getParameters().relativeUrl }; LOG(DEBUG, "Sending " << (request.getType() == ClientRequest::Type::GET ? "GET" : "POST") << " request to url '" << url << "'"); + if (!isValidUrl(url)) + { + LOG(ERROR, "Refusing request to '" << url << "': invalid URL"); + return false; + } + _client.setMaximumResponseSize(request.getParameters().onChunkReceived ? 0 : request.getParameters().responseBufferSize); bool res{}; diff --git a/src/libs/core/impl/http/UrlValidation.cpp b/src/libs/core/impl/http/UrlValidation.cpp new file mode 100644 index 00000000..d6269785 --- /dev/null +++ b/src/libs/core/impl/http/UrlValidation.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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 "core/http/UrlValidation.hpp" + +namespace lms::core::http +{ + bool isValidUrl(std::string_view url) + { + return !url.empty() && (url.starts_with("http://") || url.starts_with("https://")); + } +} // namespace lms::core::http diff --git a/src/libs/core/impl/media/Codec.cpp b/src/libs/core/impl/media/Codec.cpp index 22eaf602..83eb91e6 100644 --- a/src/libs/core/impl/media/Codec.cpp +++ b/src/libs/core/impl/media/Codec.cpp @@ -40,7 +40,7 @@ namespace lms::core::media CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false }, CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false }, CodecDesc{ Codec::Opus, "Opus", "Opus", false }, - CodecDesc{ Codec::PCM, "PCM", "Pulse-code modulation", false }, + CodecDesc{ Codec::PCM, "PCM", "Pulse-code modulation", true }, CodecDesc{ Codec::Shorten, "Shorten", "Shorten", true }, CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true }, CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false }, diff --git a/src/libs/core/include/core/String.hpp b/src/libs/core/include/core/String.hpp index 2dd2652c..3562acbb 100644 --- a/src/libs/core/include/core/String.hpp +++ b/src/libs/core/include/core/String.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include #include @@ -54,17 +55,21 @@ namespace lms::core::stringUtils [[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t\r"); [[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t\r"); + // Like str.substr(0, maxBytes), but takes utf8 into account to properly truncate + [[nodiscard]] std::string_view utf8Truncate(std::string_view str, std::size_t maxBytes); + [[nodiscard]] std::string stringToLower(std::string_view str); void stringToLower(std::string& str); [[nodiscard]] std::string stringToUpper(const std::string& str); - [[nodiscard]] std::string bufferToString(std::span data); - [[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB); [[nodiscard]] std::string_view::size_type stringCaseInsensitiveContains(std::string_view str, std::string_view strtoFind); void capitalize(std::string& str); + // returns empty string if invalid input + [[nodiscard]] std::string toRomanNumeral(std::size_t n); + template [[nodiscard]] std::optional readAs(std::string_view str) { @@ -116,7 +121,7 @@ namespace lms::core::stringUtils [[nodiscard]] bool stringEndsWith(std::string_view str, std::string_view ending); [[nodiscard]] std::optional stringFromHex(std::string_view str); - [[nodiscard]] std::string toHexString(std::string_view str); + [[nodiscard]] std::string bufferToHexString(std::span data); [[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime); [[nodiscard]] std::string toISO8601String(const Wt::WDate& date); diff --git a/src/libs/core/include/core/http/UrlValidation.hpp b/src/libs/core/include/core/http/UrlValidation.hpp new file mode 100644 index 00000000..61dadee1 --- /dev/null +++ b/src/libs/core/include/core/http/UrlValidation.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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 + +namespace lms::core::http +{ + // Returns true if url is a well-formed absolute URL with an http:// or https:// scheme. + bool isValidUrl(std::string_view url); +} // namespace lms::core::http diff --git a/src/libs/core/test/CMakeLists.txt b/src/libs/core/test/CMakeLists.txt index 074abeb0..73ce60d2 100644 --- a/src/libs/core/test/CMakeLists.txt +++ b/src/libs/core/test/CMakeLists.txt @@ -2,6 +2,7 @@ include(GoogleTest) add_executable(test-core EnumSet.cpp + HttpUrlValidation.cpp JobScheduler.cpp LiteralString.cpp PartialDateTime.cpp diff --git a/src/libs/core/test/HttpUrlValidation.cpp b/src/libs/core/test/HttpUrlValidation.cpp new file mode 100644 index 00000000..aa24df99 --- /dev/null +++ b/src/libs/core/test/HttpUrlValidation.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 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 + +#include "core/http/UrlValidation.hpp" + +namespace lms::core::http::tests +{ + TEST(HttpUrlValidation, AllowedUrls) + { + EXPECT_TRUE(isValidUrl("http://feeds.example.com/podcast.rss")); + EXPECT_TRUE(isValidUrl("https://feeds.example.com/podcast.rss")); + EXPECT_TRUE(isValidUrl("http://192.168.1.100/feed.rss")); + EXPECT_TRUE(isValidUrl("https://example.com/episode.mp3")); + } + + TEST(HttpUrlValidation, DisallowedUrls) + { + EXPECT_FALSE(isValidUrl("")); + EXPECT_FALSE(isValidUrl("ftp://attacker.com/feed")); + EXPECT_FALSE(isValidUrl("file:///etc/passwd")); + EXPECT_FALSE(isValidUrl("javascript:alert(1)")); + EXPECT_FALSE(isValidUrl("//example.com/feed")); + EXPECT_FALSE(isValidUrl("HTTP://example.com/feed")); // scheme check is case-sensitive + } +} // namespace lms::core::http::tests diff --git a/src/libs/core/test/String.cpp b/src/libs/core/test/String.cpp index 29b1d259..66da6c27 100644 --- a/src/libs/core/test/String.cpp +++ b/src/libs/core/test/String.cpp @@ -17,6 +17,9 @@ * along with LMS. If not, see . */ +#include +#include + #include #include @@ -385,6 +388,31 @@ namespace lms::core::stringUtils::tests EXPECT_FALSE(stringEndsWith("FooBar", "R")); } + TEST(StringUtils, utf8Truncate) + { + EXPECT_EQ(utf8Truncate("abc", 10), "abc"); + EXPECT_EQ(utf8Truncate("abcdef", 6), "abcdef"); + EXPECT_EQ(utf8Truncate("", 10), ""); + EXPECT_EQ(utf8Truncate("abc", 0), ""); + + EXPECT_EQ(utf8Truncate("caf\xC3\xA9", 4), "caf"); + EXPECT_EQ(utf8Truncate("caf\xC3\xA9", 5), "caf\xC3\xA9"); + + EXPECT_EQ(utf8Truncate("\xE2\x82\xAC", 1), ""); + EXPECT_EQ(utf8Truncate("\xE2\x82\xAC", 2), ""); + EXPECT_EQ(utf8Truncate("\xE2\x82\xAC", 3), "\xE2\x82\xAC"); + + EXPECT_EQ(utf8Truncate("\xF0\x9F\x98\x80", 1), ""); + EXPECT_EQ(utf8Truncate("\xF0\x9F\x98\x80", 2), ""); + EXPECT_EQ(utf8Truncate("\xF0\x9F\x98\x80", 3), ""); + EXPECT_EQ(utf8Truncate("\xF0\x9F\x98\x80", 4), "\xF0\x9F\x98\x80"); + + EXPECT_EQ(utf8Truncate("e\xCC\x81", 2), "e"); + EXPECT_EQ(utf8Truncate("e\xCC\x81", 3), "e\xCC\x81"); + + EXPECT_EQ(utf8Truncate("\x80\x80\x80", 2), ""); + } + TEST(StringUtils, stringCaseInsensitiveContains) { EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "Bar")); @@ -397,13 +425,11 @@ namespace lms::core::stringUtils::tests EXPECT_FALSE(stringCaseInsensitiveContains("", "Foo")); } - TEST(StringUtils, toHexString) + TEST(StringUtils, bufferToHexString) { - EXPECT_EQ(toHexString(""), ""); - EXPECT_EQ(toHexString("123"), "313233"); - EXPECT_EQ(toHexString("1234"), "31323334"); - EXPECT_EQ(toHexString("12345"), "3132333435"); - EXPECT_EQ(toHexString("Test"), "54657374"); + EXPECT_EQ(bufferToHexString({}), ""); + EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x31 }, std::byte{ 0x32 }, std::byte{ 0x33 } }), "313233"); + EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x00 }, std::byte{ 0xab }, std::byte{ 0xcd }, std::byte{ 0xff } }), "00ABCDFF"); // test back stringFromHex EXPECT_EQ(stringFromHex(""), ""); @@ -412,4 +438,32 @@ namespace lms::core::stringUtils::tests EXPECT_EQ(stringFromHex("3132333435"), "12345"); EXPECT_EQ(stringFromHex("54657374"), "Test"); } + + TEST(StringUtils, toRomanNumeral) + { + EXPECT_EQ(toRomanNumeral(1), "i"); + EXPECT_EQ(toRomanNumeral(2), "ii"); + EXPECT_EQ(toRomanNumeral(3), "iii"); + EXPECT_EQ(toRomanNumeral(4), "iv"); + EXPECT_EQ(toRomanNumeral(5), "v"); + EXPECT_EQ(toRomanNumeral(6), "vi"); + EXPECT_EQ(toRomanNumeral(7), "vii"); + EXPECT_EQ(toRomanNumeral(8), "viii"); + EXPECT_EQ(toRomanNumeral(9), "ix"); + EXPECT_EQ(toRomanNumeral(10), "x"); + EXPECT_EQ(toRomanNumeral(11), "xi"); + EXPECT_EQ(toRomanNumeral(14), "xiv"); + EXPECT_EQ(toRomanNumeral(16), "xvi"); + EXPECT_EQ(toRomanNumeral(40), "xl"); + EXPECT_EQ(toRomanNumeral(50), "l"); + EXPECT_EQ(toRomanNumeral(90), "xc"); + EXPECT_EQ(toRomanNumeral(99), "xcix"); + EXPECT_EQ(toRomanNumeral(444), "cdxliv"); + EXPECT_EQ(toRomanNumeral(1994), "mcmxciv"); + EXPECT_EQ(toRomanNumeral(3999), "mmmcmxcix"); + EXPECT_EQ(toRomanNumeral(0), ""); + EXPECT_EQ(toRomanNumeral(4000), ""); + EXPECT_EQ(toRomanNumeral(static_cast(-1)), ""); // underflows to SIZE_MAX + EXPECT_EQ(toRomanNumeral(std::numeric_limits::max()), ""); + } } // namespace lms::core::stringUtils::tests \ No newline at end of file diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index bc164a03..6360c809 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -24,11 +24,14 @@ add_library(lmsdatabase STATIC impl/objects/RatedTrack.cpp impl/objects/Release.cpp impl/objects/ScanSettings.cpp + impl/objects/ServerInfo.cpp impl/objects/StarredArtist.cpp impl/objects/StarredRelease.cpp impl/objects/StarredTrack.cpp + impl/objects/Movement.cpp impl/objects/Track.cpp impl/objects/TrackArtistLink.cpp + impl/objects/Work.cpp impl/objects/TrackBookmark.cpp impl/objects/TrackEmbeddedImage.cpp impl/objects/TrackEmbeddedImageLink.cpp diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index 6d242777..6779b70c 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -248,7 +248,7 @@ namespace lms::db } else if (checkType != "none") { - throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'."); + throw Exception{ "Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'." }; } } @@ -360,7 +360,7 @@ namespace lms::db }) }; if (!foreignKeyConstraintsPassed) - throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database."); + throw Exception{ "Foreign key constraints check failed! Please restore from a backup or recreate the database." }; LMS_LOG(DB, INFO, "Foreign key constraints check passed!"); } diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index dc65b75d..eec0d484 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -36,7 +36,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 108 }; + static constexpr Version LMS_DATABASE_VERSION{ 110 }; } VersionInfo::VersionInfo() @@ -1922,6 +1922,50 @@ WHERE ct.name = 'GROUPING')"); utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster_type WHERE name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING'))"); } + void migrateFromV108(Session& session) + { + utils::executeCommand(*session.getDboSession(), R"( +CREATE TABLE IF NOT EXISTS "work" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "mbid" blob +))"); + utils::executeCommand(*session.getDboSession(), R"( +CREATE TABLE IF NOT EXISTS "track_work" ( + "work_id" bigint, + "track_id" bigint, + primary key ("work_id", "track_id"), + constraint "fk_track_work_key1" foreign key ("work_id") references "work" ("id") on delete cascade deferrable initially deferred, + constraint "fk_track_work_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred +))"); + utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_work_work" on "track_work" ("work_id"))"); + utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_work_track" on "track_work" ("track_id"))"); + utils::executeCommand(*session.getDboSession(), R"( +CREATE TABLE IF NOT EXISTS "track_movement" ( + "id" integer primary key autoincrement, + "version" integer not null, + "name" text not null, + "number" integer, + "count" integer, + "track_id" bigint, + constraint "fk_track_movement_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred +))"); + + // Just increment the scan version of the settings to make the next scan rescan all audio files + utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1"); + } + + void migrateFromV109(Session& session) + { + utils::executeCommand(*session.getDboSession(), R"( +CREATE TABLE IF NOT EXISTS "server_info" ( + "id" integer primary key autoincrement, + "version" integer not null, + "instance_id" blob not null +))"); + } + bool doDbMigration(Session& session) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -2006,6 +2050,8 @@ WHERE ct.name = 'GROUPING')"); { 105, migrateFromV105 }, { 106, migrateFromV106 }, { 107, migrateFromV107 }, + { 108, migrateFromV108 }, + { 109, migrateFromV109 }, }; LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration"); diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 01c49b2b..a72f6565 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -38,6 +38,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/PlayListFile.hpp" #include "database/objects/PlayQueue.hpp" #include "database/objects/Podcast.hpp" @@ -48,6 +49,7 @@ #include "database/objects/Release.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ScanSettings.hpp" +#include "database/objects/ServerInfo.hpp" #include "database/objects/StarredArtist.hpp" #include "database/objects/StarredRelease.hpp" #include "database/objects/StarredTrack.hpp" @@ -61,6 +63,7 @@ #include "database/objects/TrackMusicNNEmbeddings.hpp" #include "database/objects/UIState.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "Db.hpp" #include "Migration.hpp" @@ -111,6 +114,7 @@ namespace lms::db _session.mapClass("starred_release"); _session.mapClass("starred_track"); _session.mapClass("track"); + _session.mapClass("track_movement"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); _session.mapClass("track_embedded_image"); @@ -120,7 +124,9 @@ namespace lms::db _session.mapClass("tracklist_entry"); _session.mapClass("track_lyrics"); _session.mapClass("ui_state"); + _session.mapClass("work"); _session.mapClass("user"); + _session.mapClass("server_info"); _session.mapClass("version_info"); } @@ -176,17 +182,22 @@ namespace lms::db bool Session::migrateSchemaIfNeeded() { - const bool migrationPerformed{ Migration::doDbMigration(*this) }; + return Migration::doDbMigration(*this); + } - // TODO: move this elsewhere - { - auto uniqueTransaction{ createWriteTransaction() }; + void Session::createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType) + { + auto uniqueTransaction{ createWriteTransaction() }; - if (!ScanSettings::find(*this)) - create(); - } + if (!ScanSettings::find(*this)) + create().modify()->setRecommendationEngineType(defaultRecommendationEngineType); + } - return migrationPerformed; + void Session::createServerInfoIfNeeded() + { + auto uniqueTransaction{ createWriteTransaction() }; + + ServerInfo::getOrCreate(*this); } void Session::createIndexesIfNeeded() @@ -322,6 +333,10 @@ namespace lms::db "CREATE INDEX IF NOT EXISTS track_lyrics_directory_idx ON track_lyrics(directory_id)", "CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)", + "CREATE INDEX IF NOT EXISTS track_movement_track_idx ON track_movement(track_id)", + + "CREATE INDEX IF NOT EXISTS work_mbid_idx ON work(mbid)", + "CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)", "CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)", diff --git a/src/libs/database/impl/Utils.hpp b/src/libs/database/impl/Utils.hpp index 3d13303c..838cc39a 100644 --- a/src/libs/database/impl/Utils.hpp +++ b/src/libs/database/impl/Utils.hpp @@ -128,29 +128,10 @@ namespace lms::db::utils } template - RangeResults execRangeQuery(Query& query, const std::optional range) + std::vector execRangeQuery(Query& query, const std::optional range) { - RangeResults res; - - if (range) - { - res.range.offset = range->offset; - applyRange(query, Range{ range->offset, range->size + 1 }); - - res.results.reserve(range->size); - } - - // TODO optim useless last copy - res.results = utils::fetchQueryResults(query); - if (range && (res.results.size() == range->size + 1)) - { - res.moreResults = true; - res.results.pop_back(); - } - - res.range.size = res.results.size(); - - return res; + applyRange(query, range); + return utils::fetchQueryResults(query); } template @@ -162,35 +143,6 @@ namespace lms::db::utils forEachQueryResult(query, std::forward(func)); } - template - void forEachQueryRangeResult(Query& query, std::optional range, bool& moreResults, UnaryFunc&& func) - { - using ResultType = typename QueryResultType::type; - - if (range) - applyRange(query, Range{ range->offset, range->size + 1 }); - - moreResults = false; - - std::size_t count{}; - ScopedQueryProfiler queryProfiler{ query }; - const auto collection{ query.resultList() }; - auto it{ fetchFirstResult(collection) }; - while (it != collection.end()) - { - if (range && (count++ == static_cast(range->size))) - { - moreResults = true; - break; - } - - queryProfiler.suspend(); - func(*it); - queryProfiler.resume(); - fetchNextResult(it); - } - } - template void executeCommand(Wt::Dbo::Session& session, std::string_view command, const Args&... args) { diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp index ea9e6b48..d2a5160c 100644 --- a/src/libs/database/impl/objects/Artist.cpp +++ b/src/libs/database/impl/objects/Artist.cpp @@ -22,6 +22,8 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Cluster.hpp" @@ -135,14 +137,13 @@ namespace lms::db if (params.starringUser.isValid()) { - assert(params.feedbackBackend); query.join("starred_artist s_a ON s_a.artist_id = a.id") + .join("user u ON u.id = s_a.user_id") .where("s_a.user_id = ?") .bind(params.starringUser) - .where("s_a.backend = ?") - .bind(*params.feedbackBackend) .where("s_a.sync_state <> ?") - .bind(SyncState::PendingRemove); + .bind(SyncState::PendingRemove) + .where("s_a.backend = u.feedback_backend"); } if (params.filters.clusters.size() == 1) @@ -160,7 +161,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId clusterId : params.filters.clusters) { - clusterClause.Or(WhereClause("t_c.cluster_id = ?")); + clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" }); query.bind(clusterId); } @@ -305,8 +306,7 @@ namespace lms::db { session.checkReadTransaction(); - if (name.size() > maxNameLength) - name = name.substr(0, maxNameLength); + name = core::stringUtils::utf8Truncate(name, maxNameLength); return utils::fetchQueryResults(session.getDboSession()->query>("SELECT a FROM artist a").where("a.name = ?").bind(name).orderBy("LENGTH(a.mbid) DESC")); // put mbid entries first } @@ -323,7 +323,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT a FROM artist a").where("a.id = ?").bind(id)); } - RangeResults Artist::findIds(Session& session, const FindParameters& params) + std::vector Artist::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -331,7 +331,7 @@ namespace lms::db return utils::execRangeQuery(query, params.range); } - RangeResults Artist::find(Session& session, const FindParameters& params) + std::vector Artist::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -357,7 +357,7 @@ namespace lms::db return IdRange{ .first = std::get<0>(res), .last = std::get<1>(res) }; } - RangeResults Artist::findOrphanIds(Session& session, std::optional range) + std::vector Artist::findOrphanIds(Session& session, std::optional range) { // TODO extend with release artists session.checkReadTransaction(); @@ -388,7 +388,7 @@ AND NOT EXISTS ( return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; } - RangeResults Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range) + std::vector Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range) { session.checkReadTransaction(); @@ -407,8 +407,8 @@ AND NOT EXISTS ( auto results{ utils::execRangeQuery(query, range) }; - if (!results.results.empty()) - lastRetrievedArtist = results.results.back()->getId(); + if (!results.empty()) + lastRetrievedArtist = results.back()->getId(); return results; } @@ -442,11 +442,11 @@ AND NOT EXISTS ( std::ostringstream oss; oss << "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 cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a_l.artist_id = a.id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"; - where.And(WhereClause("a.id = ?")).bind(getId().toString()); + where.And(WhereClause{ "a.id = ?" }).bind(getId().toString()); { WhereClause clusterClause; for (const ClusterTypeId clusterTypeId : clusterTypeIds) - clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); + clusterClause.Or(WhereClause{ "c_type.id = ?" }).bind(clusterTypeId.toString()); where.And(clusterClause); } @@ -474,13 +474,13 @@ AND NOT EXISTS ( void Artist::setName(std::string_view name) { - _name.assign(name, 0, maxNameLength); + _name = core::stringUtils::utf8Truncate(name, maxNameLength); LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Artist name too long, truncated to '" << _name << "'"); } void Artist::setSortName(std::string_view sortName) { - _sortName.assign(sortName, 0, maxNameLength); + _sortName = core::stringUtils::utf8Truncate(sortName, maxNameLength); LMS_LOG_IF(DB, WARNING, sortName.size() > maxNameLength, "Artist sort name too long, truncated to '" << _sortName << "'"); } diff --git a/src/libs/database/impl/objects/Cluster.cpp b/src/libs/database/impl/objects/Cluster.cpp index b574e798..996c8bf8 100644 --- a/src/libs/database/impl/objects/Cluster.cpp +++ b/src/libs/database/impl/objects/Cluster.cpp @@ -31,12 +31,14 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -130,7 +132,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM cluster")); } - RangeResults Cluster::findIds(Session& session, const FindParameters& params) + std::vector Cluster::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; @@ -138,7 +140,7 @@ namespace lms::db return utils::execRangeQuery(query, params.range); } - RangeResults Cluster::find(Session& session, const FindParameters& params) + std::vector Cluster::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery>(session, params) }; @@ -154,7 +156,7 @@ namespace lms::db return utils::forEachQueryResult(query, _func); } - RangeResults Cluster::findOrphanIds(Session& session, std::optional range) + std::vector Cluster::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); 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)") }; @@ -188,7 +190,7 @@ namespace lms::db _tracks.insert(getDboPtr(track)); } - RangeResults Cluster::getTracks(std::optional range) const + std::vector Cluster::getTracks(std::optional range) const { assert(session()); @@ -217,7 +219,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM cluster_type")); } - RangeResults ClusterType::findOrphanIds(Session& session, std::optional range) + std::vector ClusterType::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -229,7 +231,7 @@ namespace lms::db return utils::execRangeQuery(query, range); } - RangeResults ClusterType::findUsed(Session& session, std::optional range) + std::vector ClusterType::findUsed(Session& session, std::optional range) { session.checkReadTransaction(); @@ -263,7 +265,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); } - RangeResults ClusterType::findIds(Session& session, std::optional range) + std::vector ClusterType::findIds(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/Directory.cpp b/src/libs/database/impl/objects/Directory.cpp index 47be8b9a..822ce273 100644 --- a/src/libs/database/impl/objects/Directory.cpp +++ b/src/libs/database/impl/objects/Directory.cpp @@ -173,7 +173,7 @@ namespace lms::db }); } - RangeResults Directory::find(Session& session, const FindParameters& params) + std::vector Directory::find(Session& session, const FindParameters& params) { auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); @@ -187,7 +187,7 @@ namespace lms::db }); } - RangeResults Directory::findOrphanIds(Session& session, std::optional range) + std::vector Directory::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -208,7 +208,7 @@ namespace lms::db return utils::execRangeQuery(query, range); } - RangeResults Directory::findMismatchedLibrary(Session& session, std::optional range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId) + std::vector Directory::findMismatchedLibrary(Session& session, std::optional range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId) { session.checkReadTransaction(); @@ -219,7 +219,7 @@ namespace lms::db return utils::execRangeQuery(query, range); } - RangeResults Directory::findRootDirectories(Session& session, std::optional range) + std::vector Directory::findRootDirectories(Session& session, std::optional range) { auto query{ session.getDboSession()->query>("SELECT d from directory d").where("d.parent_directory_id IS NULL") }; return utils::execRangeQuery(query, range); diff --git a/src/libs/database/impl/objects/Genre.cpp b/src/libs/database/impl/objects/Genre.cpp index d2149ad3..85417101 100644 --- a/src/libs/database/impl/objects/Genre.cpp +++ b/src/libs/database/impl/objects/Genre.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -33,12 +34,14 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -112,7 +115,7 @@ namespace lms::db } // namespace Genre::Genre(std::string_view name) - : _name{ name.substr(0, maxNameLength) } + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } { LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Genre name too long, truncated to '" << _name << "'"); } @@ -128,14 +131,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM genre")); } - RangeResults Genre::findIds(Session& session, const FindParameters& params) + std::vector Genre::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); } - RangeResults Genre::find(Session& session, const FindParameters& params) + std::vector Genre::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery>(session, params) }; @@ -159,13 +162,12 @@ namespace lms::db { session.checkReadTransaction(); - if (name.size() > maxNameLength) - name = name.substr(0, maxNameLength); + name = core::stringUtils::utf8Truncate(name, maxNameLength); return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } - RangeResults Genre::findOrphanIds(Session& session, std::optional range) + std::vector Genre::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); auto query{ session.getDboSession()->query("SELECT g.id FROM genre g WHERE NOT EXISTS (SELECT 1 FROM track_genre t_g WHERE t_g.genre_id = g.id)") }; diff --git a/src/libs/database/impl/objects/Grouping.cpp b/src/libs/database/impl/objects/Grouping.cpp index 96f6683a..de74f0b5 100644 --- a/src/libs/database/impl/objects/Grouping.cpp +++ b/src/libs/database/impl/objects/Grouping.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -33,12 +34,14 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -112,7 +115,7 @@ namespace lms::db } // namespace Grouping::Grouping(std::string_view name) - : _name{ name.substr(0, maxNameLength) } + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } { LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Grouping name too long, truncated to '" << _name << "'"); } @@ -128,14 +131,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM grouping")); } - RangeResults Grouping::findIds(Session& session, const FindParameters& params) + std::vector Grouping::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); } - RangeResults Grouping::find(Session& session, const FindParameters& params) + std::vector Grouping::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery>(session, params) }; @@ -159,13 +162,12 @@ namespace lms::db { session.checkReadTransaction(); - if (name.size() > maxNameLength) - name = name.substr(0, maxNameLength); + name = core::stringUtils::utf8Truncate(name, maxNameLength); return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } - RangeResults Grouping::findOrphanIds(Session& session, std::optional range) + std::vector Grouping::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); auto query{ session.getDboSession()->query("SELECT g.id FROM grouping g WHERE NOT EXISTS (SELECT 1 FROM track_grouping t_gr WHERE t_gr.grouping_id = g.id)") }; diff --git a/src/libs/database/impl/objects/Image.cpp b/src/libs/database/impl/objects/Image.cpp index 152b076a..dde98c30 100644 --- a/src/libs/database/impl/objects/Image.cpp +++ b/src/libs/database/impl/objects/Image.cpp @@ -105,7 +105,7 @@ namespace lms::db }); } - RangeResults Image::find(Session& session, const FindParameters& params) + std::vector Image::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/Language.cpp b/src/libs/database/impl/objects/Language.cpp index 9a3698c2..60a32ee9 100644 --- a/src/libs/database/impl/objects/Language.cpp +++ b/src/libs/database/impl/objects/Language.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -33,12 +34,14 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -112,7 +115,7 @@ namespace lms::db } // namespace Language::Language(std::string_view name) - : _name{ name.substr(0, maxNameLength) } + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } { LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Language name too long, truncated to '" << _name << "'"); } @@ -128,14 +131,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM language")); } - RangeResults Language::findIds(Session& session, const FindParameters& params) + std::vector Language::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); } - RangeResults Language::find(Session& session, const FindParameters& params) + std::vector Language::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery>(session, params) }; @@ -159,13 +162,12 @@ namespace lms::db { session.checkReadTransaction(); - if (name.size() > maxNameLength) - name = name.substr(0, maxNameLength); + name = core::stringUtils::utf8Truncate(name, maxNameLength); return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } - RangeResults Language::findOrphanIds(Session& session, std::optional range) + std::vector Language::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); auto query{ session.getDboSession()->query("SELECT l.id FROM language l WHERE NOT EXISTS (SELECT 1 FROM track_language t_l WHERE t_l.language_id = l.id)") }; diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp index 1736dd90..a2a6f405 100644 --- a/src/libs/database/impl/objects/Listen.cpp +++ b/src/libs/database/impl/objects/Listen.cpp @@ -23,10 +23,12 @@ #include #include "database/Session.hpp" +#include "database/objects/Artist.hpp" #include "database/objects/Genre.hpp" #include "database/objects/Grouping.hpp" #include "database/objects/Language.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" @@ -46,10 +48,12 @@ namespace lms::db auto query{ session.getDboSession()->query("SELECT a.id from artist a").join("track_artist_link t_a_l ON t_a_l.artist_id = a.id").join("listen l ON l.track_id = t_a_l.track_id") }; if (params.user.isValid()) - query.where("l.user_id = ?").bind(params.user); - - if (params.backend) - query.where("l.backend = ?").bind(*params.backend); + { + query.join("user u ON u.id = l.user_id") + .where("l.user_id = ?") + .bind(params.user) + .where("l.backend = u.scrobbling_backend"); + } assert(!params.artist.isValid()); // poor check @@ -123,7 +127,7 @@ namespace lms::db WhereClause clusterClause; for (const db::ClusterId id : params.filters.clusters) { - clusterClause.Or(WhereClause("t_c.cluster_id = ?")); + clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" }); query.bind(id); } @@ -161,10 +165,12 @@ namespace lms::db auto query{ session.getDboSession()->query("SELECT r.id from release r").join("track t ON t.release_id = r.id").join("listen l ON l.track_id = t.id") }; if (params.user.isValid()) - query.where("l.user_id = ?").bind(params.user); - - if (params.backend) - query.where("l.backend = ?").bind(*params.backend); + { + query.join("user u ON u.id = l.user_id") + .where("l.user_id = ?") + .bind(params.user) + .where("l.backend = u.scrobbling_backend"); + } if (params.artist.isValid()) { @@ -223,7 +229,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId id : params.filters.clusters) { - clusterClause.Or(WhereClause("c.id = ?")); + clusterClause.Or(WhereClause{ "c.id = ?" }); query.bind(id); } @@ -244,10 +250,12 @@ namespace lms::db auto query{ session.getDboSession()->query("SELECT t.id from track t").join("listen l ON l.track_id = t.id") }; if (params.user.isValid()) - query.where("l.user_id = ?").bind(params.user); - - if (params.backend) - query.where("l.backend = ?").bind(*params.backend); + { + query.join("user u ON u.id = l.user_id") + .where("l.user_id = ?") + .bind(params.user) + .where("l.backend = u.scrobbling_backend"); + } if (params.artist.isValid()) { @@ -305,7 +313,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId id : params.filters.clusters) { - clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); + clusterClause.Or(WhereClause{ "c.id = ?" }).bind(id.toString()); query.bind(id); } @@ -348,7 +356,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT l from listen l").where("l.id = ?").bind(id)); } - RangeResults Listen::find(Session& session, const FindParameters& parameters) + std::vector Listen::find(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); @@ -373,39 +381,88 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("user_id = ?").bind(userId).where("track_id = ?").bind(trackId).where("backend = ?").bind(backend).where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))); } - RangeResults Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params) + std::vector Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params) { session.checkReadTransaction(); - auto query{ createArtistsQuery(session, params) }; - - auto collection{ query - .orderBy("COUNT(a.id) DESC") - .groupBy("a.id") }; - + auto query{ createArtistsQuery(session, params).orderBy("COUNT(a.id) DESC").groupBy("a.id") }; return utils::execRangeQuery(query, params.range); } - RangeResults Listen::getTopReleases(Session& session, const StatsFindParameters& params) + std::vector Listen::getTopReleases(Session& session, const StatsFindParameters& params) + { + session.checkReadTransaction(); + auto query{ createReleasesQuery(session, params).orderBy("COUNT(r.id) DESC").groupBy("r.id") }; + return utils::execRangeQuery(query, params.range); + } + + std::vector Listen::getTopTracks(Session& session, const StatsFindParameters& params) + { + session.checkReadTransaction(); + auto query{ createTracksQuery(session, params).orderBy("COUNT(t.id) DESC").groupBy("t.id") }; + return utils::execRangeQuery(query, params.range); + } + + std::vector Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params) + { + session.checkReadTransaction(); + auto query{ createArtistsQuery(session, params).groupBy("a.id").having("l.date_time = MAX(l.date_time)").orderBy("l.date_time DESC") }; + return utils::execRangeQuery(query, params.range); + } + + std::vector Listen::getRecentReleases(Session& session, const StatsFindParameters& params) + { + session.checkReadTransaction(); + auto query{ createReleasesQuery(session, params).groupBy("r.id").having("l.date_time = MAX(l.date_time)").orderBy("l.date_time DESC") }; + return utils::execRangeQuery(query, params.range); + } + + std::vector Listen::getRecentTracks(Session& session, const StatsFindParameters& params) + { + session.checkReadTransaction(); + auto query{ createTracksQuery(session, params).groupBy("t.id").having("l.date_time = MAX(l.date_time)").orderBy("l.date_time DESC") }; + return utils::execRangeQuery(query, params.range); + } + + void Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params, const std::function&)>& func) + { + session.checkReadTransaction(); + auto query{ createArtistsQuery(session, params) + .orderBy("COUNT(a.id) DESC") + .groupBy("a.id") }; + + utils::forEachQueryRangeResult(query, params.range, [&](const ArtistId& id) { + if (const auto artist{ Artist::find(session, id) }) + func(artist); + }); + } + + void Listen::getTopReleases(Session& session, const StatsFindParameters& params, const std::function&)>& func) { session.checkReadTransaction(); auto query{ createReleasesQuery(session, params) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryRangeResult(query, params.range, [&](const ReleaseId& id) { + if (const auto release{ Release::find(session, id) }) + func(release); + }); } - RangeResults Listen::getTopTracks(Session& session, const StatsFindParameters& params) + void Listen::getTopTracks(Session& session, const StatsFindParameters& params, const std::function&)>& func) { session.checkReadTransaction(); auto query{ createTracksQuery(session, params) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryRangeResult(query, params.range, [&](const TrackId& id) { + if (const auto track{ Track::find(session, id) }) + func(track); + }); } - RangeResults Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params) + void Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params, const std::function&)>& func) { session.checkReadTransaction(); auto query{ createArtistsQuery(session, params) @@ -413,10 +470,13 @@ namespace lms::db .having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryRangeResult(query, params.range, [&](const ArtistId& id) { + if (const auto artist{ Artist::find(session, id) }) + func(artist); + }); } - RangeResults Listen::getRecentReleases(Session& session, const StatsFindParameters& params) + void Listen::getRecentReleases(Session& session, const StatsFindParameters& params, const std::function&)>& func) { session.checkReadTransaction(); auto query{ createReleasesQuery(session, params) @@ -424,10 +484,13 @@ namespace lms::db .having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryRangeResult(query, params.range, [&](const ReleaseId& id) { + if (const auto release{ Release::find(session, id) }) + func(release); + }); } - RangeResults Listen::getRecentTracks(Session& session, const StatsFindParameters& params) + void Listen::getRecentTracks(Session& session, const StatsFindParameters& params, const std::function&)>& func) { session.checkReadTransaction(); auto query{ createTracksQuery(session, params) @@ -435,7 +498,10 @@ namespace lms::db .having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; - return utils::execRangeQuery(query, params.range); + utils::forEachQueryRangeResult(query, params.range, [&](const TrackId& id) { + if (const auto track{ Track::find(session, id) }) + func(track); + }); } std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId) diff --git a/src/libs/database/impl/objects/MediaLibrary.cpp b/src/libs/database/impl/objects/MediaLibrary.cpp index 853658ca..11520ebc 100644 --- a/src/libs/database/impl/objects/MediaLibrary.cpp +++ b/src/libs/database/impl/objects/MediaLibrary.cpp @@ -21,6 +21,8 @@ #include +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/objects/Genre.hpp" #include "database/objects/Grouping.hpp" @@ -38,7 +40,7 @@ DBO_INSTANTIATE_TEMPLATES(lms::db::MediaLibrary) namespace lms::db { MediaLibrary::MediaLibrary(std::string_view name, const std::filesystem::path& p) - : _name{ std::string{ name, 0, maxNameLength } } + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } { setPath(p); } diff --git a/src/libs/database/impl/objects/Medium.cpp b/src/libs/database/impl/objects/Medium.cpp index b98fbb72..d50970e7 100644 --- a/src/libs/database/impl/objects/Medium.cpp +++ b/src/libs/database/impl/objects/Medium.cpp @@ -31,12 +31,14 @@ #include "database/objects/Language.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -110,7 +112,7 @@ namespace lms::db return IdRange{ .first = std::get<0>(res), .last = std::get<1>(res) }; } - RangeResults Medium::findOrphanIds(Session& session, std::optional range) + std::vector Medium::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/Mood.cpp b/src/libs/database/impl/objects/Mood.cpp index 18430ba4..b338b2be 100644 --- a/src/libs/database/impl/objects/Mood.cpp +++ b/src/libs/database/impl/objects/Mood.cpp @@ -22,6 +22,7 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" @@ -33,12 +34,14 @@ #include "database/objects/Language.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" @@ -112,7 +115,7 @@ namespace lms::db } // namespace Mood::Mood(std::string_view name) - : _name{ name.substr(0, maxNameLength) } + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } { LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Mood name too long, truncated to '" << _name << "'"); } @@ -128,14 +131,14 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM mood")); } - RangeResults Mood::findIds(Session& session, const FindParameters& params) + std::vector Mood::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); } - RangeResults Mood::find(Session& session, const FindParameters& params) + std::vector Mood::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery>(session, params) }; @@ -159,13 +162,12 @@ namespace lms::db { session.checkReadTransaction(); - if (name.size() > maxNameLength) - name = name.substr(0, maxNameLength); + name = core::stringUtils::utf8Truncate(name, maxNameLength); return utils::fetchQuerySingleResult(session.getDboSession()->find().where("name = ?").bind(name)); } - RangeResults Mood::findOrphanIds(Session& session, std::optional range) + std::vector Mood::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); auto query{ session.getDboSession()->query("SELECT m.id FROM mood m WHERE NOT EXISTS (SELECT 1 FROM track_mood t_m WHERE t_m.mood_id = m.id)") }; diff --git a/src/libs/database/impl/objects/Movement.cpp b/src/libs/database/impl/objects/Movement.cpp new file mode 100644 index 00000000..5afe983c --- /dev/null +++ b/src/libs/database/impl/objects/Movement.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 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 "database/objects/Movement.hpp" + +#include + +#include "core/ILogger.hpp" +#include "core/String.hpp" + +#include "database/Session.hpp" +#include "database/objects/Artist.hpp" +#include "database/objects/Artwork.hpp" +#include "database/objects/Cluster.hpp" +#include "database/objects/Directory.hpp" +#include "database/objects/Genre.hpp" +#include "database/objects/Grouping.hpp" +#include "database/objects/Language.hpp" +#include "database/objects/MediaLibrary.hpp" +#include "database/objects/Medium.hpp" +#include "database/objects/Mood.hpp" +#include "database/objects/Release.hpp" +#include "database/objects/Track.hpp" +#include "database/objects/TrackArtistLink.hpp" +#include "database/objects/TrackEmbeddedImage.hpp" +#include "database/objects/TrackEmbeddedImageLink.hpp" +#include "database/objects/TrackLyrics.hpp" + +#include "traits/IdTypeTraits.hpp" + +DBO_INSTANTIATE_TEMPLATES(lms::db::Movement) + +namespace lms::db +{ + Movement::Movement(std::string_view name, std::optional number, std::optional count, const ObjectPtr& track) + : _name{ core::stringUtils::utf8Truncate(name, maxNameLength) } + , _number{ number } + , _count{ count } + , _track{ getDboPtr(track) } + { + LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Movement name too long, truncated to '" << _name << "'"); + } + + Movement::pointer Movement::create(Session& session, std::string_view name, std::optional number, std::optional count, const ObjectPtr& track) + { + return session.getDboSession()->add(std::unique_ptr{ new Movement{ name, number, count, track } }); + } + +} // namespace lms::db diff --git a/src/libs/database/impl/objects/PlayListFile.cpp b/src/libs/database/impl/objects/PlayListFile.cpp index 702e6918..a09cf1d0 100644 --- a/src/libs/database/impl/objects/PlayListFile.cpp +++ b/src/libs/database/impl/objects/PlayListFile.cpp @@ -23,6 +23,8 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Directory.hpp" @@ -171,7 +173,7 @@ namespace lms::db void PlayListFile::setName(std::string_view name) { - _name = std::string{ name, 0, _maxNameLength }; + _name = core::stringUtils::utf8Truncate(name, _maxNameLength); LMS_LOG_IF(DB, WARNING, name.size() > _maxNameLength, "PlaylistFile name too long, truncated to '" << _name << "'"); } diff --git a/src/libs/database/impl/objects/PlayQueue.cpp b/src/libs/database/impl/objects/PlayQueue.cpp index ffa824ab..25145a85 100644 --- a/src/libs/database/impl/objects/PlayQueue.cpp +++ b/src/libs/database/impl/objects/PlayQueue.cpp @@ -33,6 +33,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" @@ -40,6 +41,7 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "Utils.hpp" #include "traits/IdTypeTraits.hpp" diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index 71c5f402..f50b49d9 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -23,6 +23,8 @@ #include #include "core/PartialDateTime.hpp" +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" @@ -35,6 +37,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" @@ -42,6 +45,7 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" @@ -93,6 +97,9 @@ namespace lms::db query.join("track t ON t.release_id = r.id"); } + if (!params.keywords.empty()) + query.leftJoin("medium m ON m.release_id = r.id"); + if (params.parentDirectory.isValid()) { query.join("directory d ON t.directory_id = d.id"); @@ -145,19 +152,35 @@ namespace lms::db if (!params.name.empty()) query.where("r.name = ?").bind(params.name); - for (std::string_view keyword : params.keywords) - query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + if (!params.keywords.empty()) + { + std::vector nameClauses; + std::vector mediumNameClauses; + + for (const std::string_view keyword : params.keywords) + { + nameClauses.push_back("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); + query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + } + + for (const std::string_view keyword : params.keywords) + { + mediumNameClauses.push_back("m.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); + query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + } + + query.where("(" + core::stringUtils::joinStrings(nameClauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(mediumNameClauses, " AND ") + ")"); + } if (params.starringUser.isValid()) { - assert(params.feedbackBackend); query.join("starred_release s_r ON s_r.release_id = r.id") + .join("user u ON u.id = s_r.user_id") .where("s_r.user_id = ?") .bind(params.starringUser) - .where("s_r.backend = ?") - .bind(*params.feedbackBackend) .where("s_r.sync_state <> ?") - .bind(SyncState::PendingRemove); + .bind(SyncState::PendingRemove) + .where("s_r.backend = u.feedback_backend"); } if (params.artist.isValid()) @@ -213,7 +236,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId clusterId : params.filters.clusters) { - clusterClause.Or(WhereClause("t_c.cluster_id = ?")); + clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" }); query.bind(clusterId); } @@ -356,7 +379,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT c from country c").where("c.name = ?").bind(name)); } - RangeResults Country::findOrphanIds(Session& session, std::optional range) + std::vector Country::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -418,7 +441,7 @@ namespace lms::db }); } - RangeResults Label::findOrphanIds(Session& session, std::optional range) + std::vector Label::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -480,7 +503,7 @@ namespace lms::db }); } - RangeResults ReleaseType::findOrphanIds(Session& session, std::optional range) + std::vector ReleaseType::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -490,7 +513,7 @@ namespace lms::db } Release::Release(const std::string& name, const std::optional& MBID) - : _name{ std::string(name, 0, _maxNameLength) } + : _name{ core::stringUtils::utf8Truncate(name, _maxNameLength) } , _MBID{ MBID } { } @@ -527,7 +550,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM release")); } - RangeResults Release::findOrphanIds(Session& session, std::optional range) + std::vector Release::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); @@ -574,7 +597,7 @@ namespace lms::db return IdRange{ .first = std::get<0>(res), .last = std::get<1>(res) }; } - RangeResults Release::find(Session& session, const FindParameters& params) + std::vector Release::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -590,7 +613,7 @@ namespace lms::db utils::forEachQueryRangeResult(query, params.range, func); } - RangeResults Release::findIds(Session& session, const FindParameters& params) + std::vector Release::findIds(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -980,11 +1003,11 @@ namespace lms::db oss << "SELECT c from cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id "; - where.And(WhereClause("t.release_id = ?")).bind(getId().toString()); + where.And(WhereClause{ "t.release_id = ?" }).bind(getId().toString()); { WhereClause clusterClause; for (const ClusterTypeId clusterTypeId : clusterTypeIds) - clusterClause.Or(WhereClause("c.cluster_type_id = ?")).bind(clusterTypeId.toString()); + clusterClause.Or(WhereClause{ "c.cluster_type_id = ?" }).bind(clusterTypeId.toString()); where.And(clusterClause); } oss << " " << where.get(); diff --git a/src/libs/database/impl/objects/ReleaseArtistLink.cpp b/src/libs/database/impl/objects/ReleaseArtistLink.cpp index b6616f9b..00e49d10 100644 --- a/src/libs/database/impl/objects/ReleaseArtistLink.cpp +++ b/src/libs/database/impl/objects/ReleaseArtistLink.cpp @@ -22,6 +22,8 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Genre.hpp" @@ -149,13 +151,13 @@ namespace lms::db void ReleaseArtistLink::setArtistName(std::string_view artistName) { - _artistName.assign(artistName, 0, Artist::maxNameLength); + _artistName = core::stringUtils::utf8Truncate(artistName, Artist::maxNameLength); LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'"); } void ReleaseArtistLink::setArtistSortName(std::string_view artistSortName) { - _artistSortName.assign(artistSortName, 0, Artist::maxNameLength); + _artistSortName = core::stringUtils::utf8Truncate(artistSortName, Artist::maxNameLength); LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'"); } } // namespace lms::db diff --git a/src/libs/database/impl/objects/ServerInfo.cpp b/src/libs/database/impl/objects/ServerInfo.cpp new file mode 100644 index 00000000..cffa8dcb --- /dev/null +++ b/src/libs/database/impl/objects/ServerInfo.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2026 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 "database/objects/ServerInfo.hpp" + +#include +#include + +#include "database/Session.hpp" + +#include "Utils.hpp" +#include "traits/UUIDTraits.hpp" + +DBO_INSTANTIATE_TEMPLATES(lms::db::ServerInfo) + +namespace lms::db +{ + ServerInfo::ServerInfo(core::UUID instanceId) + : _instanceId{ instanceId } + { + } + + ServerInfo::pointer ServerInfo::getOrCreate(Session& session) + { + session.checkWriteTransaction(); + + pointer serverInfo{ utils::fetchQuerySingleResult(session.getDboSession()->find()) }; + if (!serverInfo) + return session.getDboSession()->add(std::unique_ptr{ new ServerInfo{ core::UUID::generate() } }); + + return serverInfo; + } + + ServerInfo::pointer ServerInfo::get(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->find()); + } +} // namespace lms::db diff --git a/src/libs/database/impl/objects/StarredTrack.cpp b/src/libs/database/impl/objects/StarredTrack.cpp index ed6ebecf..c21b2682 100644 --- a/src/libs/database/impl/objects/StarredTrack.cpp +++ b/src/libs/database/impl/objects/StarredTrack.cpp @@ -78,7 +78,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT 1 from starred_track").where("track_id = ?").bind(trackId).where("user_id = ?").bind(userId).where("backend = ?").bind(backend)); } - RangeResults StarredTrack::find(Session& session, const FindParameters& params) + std::vector StarredTrack::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index 2e95d3ce..f188351c 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -23,6 +23,7 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" #include "database/Session.hpp" #include "database/Types.hpp" @@ -36,12 +37,14 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" @@ -77,14 +80,13 @@ namespace lms::db if (params.starringUser.isValid()) { - assert(params.feedbackBackend); query.join("starred_track s_t ON s_t.track_id = t.id") + .join("user u ON u.id = s_t.user_id") .where("s_t.user_id = ?") .bind(params.starringUser) - .where("s_t.backend = ?") - .bind(*params.feedbackBackend) .where("s_t.sync_state <> ?") - .bind(SyncState::PendingRemove); + .bind(SyncState::PendingRemove) + .where("s_t.backend = u.feedback_backend"); } if (params.filters.clusters.size() == 1) @@ -103,7 +105,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId clusterId : params.filters.clusters) { - clusterClause.Or(WhereClause("t_c.cluster_id = ?")); + clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" }); query.bind(clusterId); } @@ -435,7 +437,7 @@ namespace lms::db return utils::fetchQueryResults(session.getDboSession()->query>("SELECT t from track t").where("t.recording_mbid = ?").bind(mbid)); } - RangeResults Track::findIdsTrackMBIDDuplicates(Session& session, std::optional range) + std::vector Track::findIdsTrackMBIDDuplicates(Session& session, std::optional range) { session.checkReadTransaction(); @@ -564,7 +566,7 @@ namespace lms::db return _preferredMediaArtwork.id(); } - RangeResults Track::findIds(Session& session, const FindParameters& parameters) + std::vector Track::findIds(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); @@ -572,7 +574,7 @@ namespace lms::db return utils::execRangeQuery(query, parameters.range); } - RangeResults Track::find(Session& session, const FindParameters& parameters) + std::vector Track::find(Session& session, const FindParameters& parameters) { session.checkReadTransaction(); @@ -588,14 +590,6 @@ namespace lms::db utils::forEachQueryRangeResult(query, params.range, func); } - void Track::find(Session& session, const FindParameters& params, bool& moreResults, const std::function& func) - { - session.checkReadTransaction(); - - auto query{ createQuery>(session, params) }; - utils::forEachQueryRangeResult(query, params.range, moreResults, func); - } - std::size_t Track::getCount(Session& session, const FindParameters& params) { session.checkReadTransaction(); @@ -621,19 +615,19 @@ namespace lms::db void Track::setName(std::string_view name) { - _name = std::string{ name, 0, _maxNameLength }; + _name = core::stringUtils::utf8Truncate(name, _maxNameLength); LMS_LOG_IF(DB, WARNING, name.size() > _maxNameLength, "Track name too long, truncated to '" << _name << "'"); } void Track::setCopyright(std::string_view copyright) { - _copyright = std::string{ copyright, 0, _maxCopyrightLength }; + _copyright = core::stringUtils::utf8Truncate(copyright, _maxCopyrightLength); LMS_LOG_IF(DB, WARNING, copyright.size() > _maxCopyrightLength, "Track copyright too long, truncated to '" << _copyright << "'"); } void Track::setCopyrightURL(std::string_view copyrightURL) { - _copyrightURL = std::string{ copyrightURL, 0, _maxCopyrightURLLength }; + _copyrightURL = core::stringUtils::utf8Truncate(copyrightURL, _maxCopyrightURLLength); LMS_LOG_IF(DB, WARNING, copyrightURL.size() > _maxCopyrightURLLength, "Track copyright URL too long, truncated to '" << _copyrightURL << "'"); } @@ -682,6 +676,40 @@ namespace lms::db _moods.insert(getDboPtr(mood)); } + void Track::setWorks(std::span> works) + { + _works.clear(); + for (const ObjectPtr& work : works) + _works.insert(getDboPtr(work)); + } + + std::vector Track::getWorks() const + { + // deterministic order, callers rely on the first entry + return utils::fetchQueryResults(_works.find().orderBy("id")); + } + + bool Track::hasWork() const + { + return !_works.empty(); + } + + void Track::clearMovements() + { + _movements.clear(); + } + + std::vector Track::getMovements() const + { + // deterministic order, callers rely on the first entry + return utils::fetchQueryResults(_movements.find().orderBy("id")); + } + + bool Track::hasMovement() const + { + return !_movements.empty(); + } + void Track::clearLyrics() { _trackLyrics.clear(); @@ -869,11 +897,11 @@ namespace lms::db oss << "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 cluster_type c_type ON c.cluster_type_id = c_type.id"; - where.And(WhereClause("t.id = ?")).bind(getId().toString()); + where.And(WhereClause{ "t.id = ?" }).bind(getId().toString()); { WhereClause clusterClause; for (ClusterTypeId clusterTypeId : clusterTypeIds) - clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); + clusterClause.Or(WhereClause{ "c_type.id = ?" }).bind(clusterTypeId.toString()); where.And(clusterClause); } oss << " " << where.get(); diff --git a/src/libs/database/impl/objects/TrackArtistLink.cpp b/src/libs/database/impl/objects/TrackArtistLink.cpp index e4e1a238..6b32305f 100644 --- a/src/libs/database/impl/objects/TrackArtistLink.cpp +++ b/src/libs/database/impl/objects/TrackArtistLink.cpp @@ -22,6 +22,8 @@ #include #include "core/ILogger.hpp" +#include "core/String.hpp" + #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Genre.hpp" @@ -191,13 +193,13 @@ namespace lms::db void TrackArtistLink::setArtistName(std::string_view artistName) { - _artistName.assign(artistName, 0, Artist::maxNameLength); + _artistName = core::stringUtils::utf8Truncate(artistName, Artist::maxNameLength); LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'"); } void TrackArtistLink::setArtistSortName(std::string_view artistSortName) { - _artistSortName.assign(artistSortName, 0, Artist::maxNameLength); + _artistSortName = core::stringUtils::utf8Truncate(artistSortName, Artist::maxNameLength); LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'"); } } // namespace lms::db diff --git a/src/libs/database/impl/objects/TrackBookmark.cpp b/src/libs/database/impl/objects/TrackBookmark.cpp index 7da5f4fc..5ca7f1a5 100644 --- a/src/libs/database/impl/objects/TrackBookmark.cpp +++ b/src/libs/database/impl/objects/TrackBookmark.cpp @@ -54,7 +54,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_bookmark")); } - RangeResults TrackBookmark::find(Session& session, UserId userId, std::optional range) + std::vector TrackBookmark::find(Session& session, UserId userId, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp index 5a75f034..cb971bd6 100644 --- a/src/libs/database/impl/objects/TrackEmbeddedImage.cpp +++ b/src/libs/database/impl/objects/TrackEmbeddedImage.cpp @@ -27,7 +27,9 @@ #include "database/objects/Grouping.hpp" #include "database/objects/Language.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Track.hpp" +#include "database/objects/Work.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp" @@ -150,7 +152,7 @@ namespace lms::db return utils::fetchQuerySingleResult(query); } - RangeResults TrackEmbeddedImage::findOrphanIds(Session& session, std::optional range) + std::vector TrackEmbeddedImage::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/TrackList.cpp b/src/libs/database/impl/objects/TrackList.cpp index bfdf6b5a..506885da 100644 --- a/src/libs/database/impl/objects/TrackList.cpp +++ b/src/libs/database/impl/objects/TrackList.cpp @@ -29,9 +29,11 @@ #include "database/objects/Grouping.hpp" #include "database/objects/Language.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/PlayListFile.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" @@ -113,7 +115,7 @@ namespace lms::db WhereClause clusterClause; for (const ClusterId clusterId : params.filters.clusters) { - clusterClause.Or(WhereClause("c.id = ?")); + clusterClause.Or(WhereClause{ "c.id = ?" }); query.bind(clusterId); } @@ -182,7 +184,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query>("select t_l from tracklist t_l").where("t_l.name = ?").bind(name).where("t_l.type = ?").bind(type).where("t_l.user_id = ?").bind(userId)); } - RangeResults TrackList::find(Session& session, const FindParameters& params) + std::vector TrackList::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); auto query{ createQuery(session, params) }; @@ -218,13 +220,13 @@ namespace lms::db TrackListEntry::pointer res; auto entries = getEntries(Range{ pos, 1 }); - if (!entries.results.empty()) - res = entries.results.front(); + if (!entries.empty()) + res = entries.front(); return res; } - RangeResults> TrackList::getEntries(std::optional range) const + std::vector> TrackList::getEntries(std::optional range) const { assert(session()); diff --git a/src/libs/database/impl/objects/TrackLyrics.cpp b/src/libs/database/impl/objects/TrackLyrics.cpp index 666492a2..59576066 100644 --- a/src/libs/database/impl/objects/TrackLyrics.cpp +++ b/src/libs/database/impl/objects/TrackLyrics.cpp @@ -121,7 +121,7 @@ namespace lms::db }); } - RangeResults TrackLyrics::findOrphanIds(Session& session, std::optional range) + std::vector TrackLyrics::findOrphanIds(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp b/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp index 214afc43..a9c2a153 100644 --- a/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp +++ b/src/libs/database/impl/objects/TrackMusicNNEmbeddings.cpp @@ -66,7 +66,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->find().where("track_id = ?").bind(trackId)); } - RangeResults TrackMusicNNEmbeddings::find(Session& session, std::optional range) + std::vector TrackMusicNNEmbeddings::find(Session& session, std::optional range) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/User.cpp b/src/libs/database/impl/objects/User.cpp index 1de617f3..050a5a68 100644 --- a/src/libs/database/impl/objects/User.cpp +++ b/src/libs/database/impl/objects/User.cpp @@ -61,7 +61,7 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM user")); } - RangeResults User::find(Session& session, const FindParameters& params) + std::vector User::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/objects/Work.cpp b/src/libs/database/impl/objects/Work.cpp new file mode 100644 index 00000000..9cb7a8a7 --- /dev/null +++ b/src/libs/database/impl/objects/Work.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 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 "database/objects/Work.hpp" + +#include + +#include "core/ILogger.hpp" +#include "core/String.hpp" + +#include "database/Session.hpp" +#include "database/objects/Artist.hpp" +#include "database/objects/Artwork.hpp" +#include "database/objects/Cluster.hpp" +#include "database/objects/Directory.hpp" +#include "database/objects/Genre.hpp" +#include "database/objects/Grouping.hpp" +#include "database/objects/Language.hpp" +#include "database/objects/MediaLibrary.hpp" +#include "database/objects/Medium.hpp" +#include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" +#include "database/objects/Release.hpp" +#include "database/objects/Track.hpp" +#include "database/objects/TrackArtistLink.hpp" +#include "database/objects/TrackEmbeddedImage.hpp" +#include "database/objects/TrackEmbeddedImageLink.hpp" +#include "database/objects/TrackLyrics.hpp" + +#include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" +#include "traits/UUIDTraits.hpp" + +DBO_INSTANTIATE_TEMPLATES(lms::db::Work) + +namespace lms::db +{ + Work::Work(std::string_view name, const std::optional& mbid) + : _mbid{ mbid } + { + setName(name); + } + + void Work::setName(std::string_view name) + { + _name = core::stringUtils::utf8Truncate(name, maxNameLength); + LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Work name too long, truncated to '" << _name << "'"); + } + + Work::pointer Work::create(Session& session, std::string_view name, const std::optional& mbid) + { + return session.getDboSession()->add(std::unique_ptr{ new Work{ name, mbid } }); + } + + Work::pointer Work::find(Session& session, WorkId id) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("id = ?").bind(id)); + } + + Work::pointer Work::find(Session& session, const core::UUID& mbid) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->find().where("mbid = ?").bind(mbid)); + } + + Work::pointer Work::find(Session& session, ReleaseId releaseId, std::string_view name) + { + session.checkReadTransaction(); + + name = core::stringUtils::utf8Truncate(name, maxNameLength); + + auto query{ + session.getDboSession()->query>("SELECT w FROM work w") + // clang-format off + .join("track_work t_w ON t_w.work_id = w.id") + .join("track t ON t.id = t_w.track_id") + .where("t.release_id = ?").bind(releaseId) + .where("w.name = ?").bind(std::string{ name }) + .where("w.mbid IS NULL") + .groupBy("w.id") + // clang-format on + }; + + return utils::fetchQuerySingleResult(query); + } + + std::vector Work::findOrphanIds(Session& session, std::optional range) + { + session.checkReadTransaction(); + auto query{ session.getDboSession()->query("SELECT w.id FROM work w WHERE NOT EXISTS (SELECT 1 FROM track_work t_w WHERE t_w.work_id = w.id)") }; + return utils::execRangeQuery(query, range); + } + +} // namespace lms::db diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index a551b17d..ac6ebd32 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -28,6 +28,7 @@ #include "database/Transaction.hpp" #include "database/Types.hpp" +#include "database/objects/Types.hpp" namespace lms::db { @@ -58,6 +59,8 @@ namespace lms::db void prepareTablesIfNeeded(); // need to run only once at startup bool migrateSchemaIfNeeded(); // returns true if migration was performed + void createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType = RecommendationEngineType::Clusters); + void createServerInfoIfNeeded(); void createIndexesIfNeeded(); void vacuumIfNeeded(); void vacuum(); diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 46c1661d..849c79cd 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -38,8 +38,6 @@ namespace lms::db // Request: // size = 0 => means we don't want data - // Response (via RangeResults) - // size => results size struct Range { std::size_t offset{}; @@ -65,40 +63,6 @@ namespace lms::db } } - template - struct RangeResults - { - Range range; - std::vector results; - bool moreResults{}; - - RangeResults getSubRange(Range subRange) - { - assert(subRange.offset >= range.offset); - - if (!subRange.size) - subRange.size = 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); - - RangeResults subResults; - - 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)); - - subResults.range = subRange; - if (subRange.offset + subRange.size == range.offset + range.size) - subResults.moreResults = moreResults; - else - subResults.moreResults = true; - - return subResults; - } - }; - struct FileStats { std::size_t trackCount; diff --git a/src/libs/database/include/database/objects/Artist.hpp b/src/libs/database/include/database/objects/Artist.hpp index 8786c15f..e22cd63f 100644 --- a/src/libs/database/include/database/objects/Artist.hpp +++ b/src/libs/database/include/database/objects/Artist.hpp @@ -66,9 +66,8 @@ namespace lms::db ArtistSortMethod sortMethod{ ArtistSortMethod::None }; std::optional range; Wt::WDateTime writtenAfter; - UserId starringUser; // only artists starred by this user - std::optional feedbackBackend; // and for this feedback backend - TrackId track; // artists involved in this track + UserId starringUser; // only artists starred by this user (uses their current feedback backend) + TrackId track; // artists involved in this track FindParameters& setFilters(const Filters& _filters) { @@ -105,10 +104,9 @@ namespace lms::db writtenAfter = _after; return *this; } - FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) + FindParameters& setStarringUser(UserId _user) { starringUser = _user; - feedbackBackend = _feedbackBackend; return *this; } FindParameters& setTrack(TrackId _track) @@ -127,13 +125,13 @@ namespace lms::db static std::vector find(Session& session, std::string_view name); // exact match on name field static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function& func, MediaLibraryId library = {}); static void find(Session& session, const IdRange& idRange, const std::function& func); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, std::function func); static IdRange findNextIdRange(Session& session, ArtistId lastRetrievedId, std::size_t count); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // No track related + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // No track related static bool exists(Session& session, ArtistId id); - static RangeResults findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range = std::nullopt); + static std::vector findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional range = std::nullopt); // Updates static void updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId); diff --git a/src/libs/database/include/database/objects/Cluster.hpp b/src/libs/database/include/database/objects/Cluster.hpp index 6a53e9eb..29b80a3d 100644 --- a/src/libs/database/include/database/objects/Cluster.hpp +++ b/src/libs/database/include/database/objects/Cluster.hpp @@ -91,11 +91,11 @@ namespace lms::db // Find utility static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector 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 findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // May be very slow static std::size_t computeTrackCount(Session& session, ClusterId id); @@ -104,7 +104,7 @@ namespace lms::db // Accessors std::string_view getName() const { return _name; } ObjectPtr getType() const { return _clusterType; } - RangeResults getTracks(std::optional range = std::nullopt) const; + std::vector getTracks(std::optional range = std::nullopt) const; void addTrack(ObjectPtr track); template @@ -136,12 +136,12 @@ namespace lms::db // Getters static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, std::optional range = std::nullopt); + static std::vector findIds(Session& session, std::optional range = std::nullopt); static void find(Session& session, const std::function& func); static pointer find(Session& session, std::string_view name); static pointer find(Session& session, ClusterTypeId id); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); - static RangeResults findUsed(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findUsed(Session& session, std::optional range = std::nullopt); static void remove(Session& session, const std::string& name); diff --git a/src/libs/database/include/database/objects/Directory.hpp b/src/libs/database/include/database/objects/Directory.hpp index cbc46971..dd30df7c 100644 --- a/src/libs/database/include/database/objects/Directory.hpp +++ b/src/libs/database/include/database/objects/Directory.hpp @@ -122,11 +122,11 @@ namespace lms::db static pointer find(Session& session, DirectoryId id); static pointer find(Session& session, const std::filesystem::path& path); static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function& func); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, const std::function& func); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); - static RangeResults findMismatchedLibrary(Session& session, std::optional range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId); - static RangeResults findRootDirectories(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findMismatchedLibrary(Session& session, std::optional range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId); + static std::vector findRootDirectories(Session& session, std::optional range = std::nullopt); // getters const std::filesystem::path& getAbsolutePath() const { return _absolutePath; } diff --git a/src/libs/database/include/database/objects/Genre.hpp b/src/libs/database/include/database/objects/Genre.hpp index 3c4ed88a..ec5080f1 100644 --- a/src/libs/database/include/database/objects/Genre.hpp +++ b/src/libs/database/include/database/objects/Genre.hpp @@ -82,12 +82,12 @@ namespace lms::db Genre() = default; static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, std::function func); static pointer find(Session& session, GenreId id); static pointer find(Session& session, std::string_view name); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); static std::size_t computeTrackCount(Session& session, GenreId id); static std::size_t computeReleaseCount(Session& session, GenreId id); diff --git a/src/libs/database/include/database/objects/Grouping.hpp b/src/libs/database/include/database/objects/Grouping.hpp index 26d758c7..cd592a0d 100644 --- a/src/libs/database/include/database/objects/Grouping.hpp +++ b/src/libs/database/include/database/objects/Grouping.hpp @@ -82,12 +82,12 @@ namespace lms::db Grouping() = default; static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, std::function func); static pointer find(Session& session, GroupingId id); static pointer find(Session& session, std::string_view name); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Image.hpp b/src/libs/database/include/database/objects/Image.hpp index fcbdd51d..5dcf1156 100644 --- a/src/libs/database/include/database/objects/Image.hpp +++ b/src/libs/database/include/database/objects/Image.hpp @@ -73,7 +73,7 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer find(Session& session, ImageId id); static pointer find(Session& session, const std::filesystem::path& file); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& parameters, const std::function& func); static void find(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function& func); static void findAbsoluteFilePath(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function& func); diff --git a/src/libs/database/include/database/objects/Language.hpp b/src/libs/database/include/database/objects/Language.hpp index b5db8178..b31d69d2 100644 --- a/src/libs/database/include/database/objects/Language.hpp +++ b/src/libs/database/include/database/objects/Language.hpp @@ -82,12 +82,12 @@ namespace lms::db Language() = default; static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, std::function func); static pointer find(Session& session, LanguageId id); static pointer find(Session& session, std::string_view name); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Listen.hpp b/src/libs/database/include/database/objects/Listen.hpp index 15863d49..e15dbca9 100644 --- a/src/libs/database/include/database/objects/Listen.hpp +++ b/src/libs/database/include/database/objects/Listen.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -36,6 +37,8 @@ namespace lms::db { + class Artist; + class Release; class Session; class Track; class User; @@ -78,13 +81,12 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer find(Session& session, ListenId id); static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime); - static RangeResults find(Session& session, const FindParameters& parameters); + static std::vector find(Session& session, const FindParameters& parameters); // Stats struct StatsFindParameters { UserId user; - std::optional backend; db::Filters filters; std::vector keywords; // if non empty, name must match all of these keywords std::optional range; @@ -95,11 +97,6 @@ namespace lms::db user = _user; return *this; } - StatsFindParameters& setScrobblingBackend(std::optional _backend) - { - backend = _backend; - return *this; - } StatsFindParameters& setFilters(const db::Filters& _filters) { filters = _filters; @@ -139,13 +136,19 @@ namespace lms::db } }; - static RangeResults getTopArtists(Session& session, const ArtistStatsFindParameters& params); - static RangeResults getTopReleases(Session& session, const StatsFindParameters& params); - static RangeResults getTopTracks(Session& session, const StatsFindParameters& params); + static std::vector getTopArtists(Session& session, const ArtistStatsFindParameters& params); + static std::vector getTopReleases(Session& session, const StatsFindParameters& params); + static std::vector getTopTracks(Session& session, const StatsFindParameters& params); + static std::vector getRecentArtists(Session& session, const ArtistStatsFindParameters& params); + static std::vector getRecentReleases(Session& session, const StatsFindParameters& params); + static std::vector getRecentTracks(Session& session, const StatsFindParameters& params); - static RangeResults getRecentArtists(Session& session, const ArtistStatsFindParameters& params); - static RangeResults getRecentReleases(Session& session, const StatsFindParameters& params); - static RangeResults getRecentTracks(Session& session, const StatsFindParameters& params); + static void getTopArtists(Session& session, const ArtistStatsFindParameters& params, const std::function&)>& func); + static void getTopReleases(Session& session, const StatsFindParameters& params, const std::function&)>& func); + static void getTopTracks(Session& session, const StatsFindParameters& params, const std::function&)>& func); + static void getRecentArtists(Session& session, const ArtistStatsFindParameters& params, const std::function&)>& func); + static void getRecentReleases(Session& session, const StatsFindParameters& params, const std::function&)>& func); + static void getRecentTracks(Session& session, const StatsFindParameters& params, const std::function&)>& func); static std::size_t getCount(Session& session, UserId userId, TrackId trackId); // for the current backend static std::size_t getCount(Session& session, UserId userId, ReleaseId trackId); // for the current backend diff --git a/src/libs/database/include/database/objects/Medium.hpp b/src/libs/database/include/database/objects/Medium.hpp index 55c3dff5..afa95e13 100644 --- a/src/libs/database/include/database/objects/Medium.hpp +++ b/src/libs/database/include/database/objects/Medium.hpp @@ -78,7 +78,7 @@ namespace lms::db static pointer find(Session& session, ReleaseId id, std::optional position); static void find(Session& session, const IdRange& idRange, const std::function& func); static IdRange findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // Updates static void updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId); diff --git a/src/libs/database/include/database/objects/Mood.hpp b/src/libs/database/include/database/objects/Mood.hpp index f12b4ea6..81f1f4c6 100644 --- a/src/libs/database/include/database/objects/Mood.hpp +++ b/src/libs/database/include/database/objects/Mood.hpp @@ -82,12 +82,12 @@ namespace lms::db Mood() = default; static std::size_t getCount(Session& session); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, std::function func); static pointer find(Session& session, MoodId id); static pointer find(Session& session, std::string_view name); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/objects/Movement.hpp b/src/libs/database/include/database/objects/Movement.hpp new file mode 100644 index 00000000..fb33a9ba --- /dev/null +++ b/src/libs/database/include/database/objects/Movement.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2025 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 + +#include + +#include "database/Object.hpp" +#include "database/objects/MovementId.hpp" +#include "database/objects/TrackId.hpp" + +namespace lms::db +{ + class Session; + class Track; + + class Movement final : public Object + { + public: + static constexpr std::size_t maxNameLength{ 512 }; + + Movement() = default; + + static pointer create(Session& session, std::string_view name, std::optional number, std::optional count, const ObjectPtr& track); + + std::string_view getName() const { return _name; } + std::optional getNumber() const { return _number; } + std::optional getCount() const { return _count; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _number, "number"); + Wt::Dbo::field(a, _count, "count"); + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + } + + private: + friend class Session; + Movement(std::string_view name, std::optional number, std::optional count, const ObjectPtr& track); + + std::string _name; + std::optional _number; + std::optional _count; + Wt::Dbo::ptr _track; + }; + +} // namespace lms::db diff --git a/src/libs/database/include/database/objects/MovementId.hpp b/src/libs/database/include/database/objects/MovementId.hpp new file mode 100644 index 00000000..264f3789 --- /dev/null +++ b/src/libs/database/include/database/objects/MovementId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 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 "database/IdType.hpp" + +LMS_DECLARE_IDTYPE(MovementId) diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index 95a0d1ae..468580cd 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -70,7 +70,7 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer find(Session& session, CountryId id); static pointer find(Session& session, std::string_view name); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // Accessors std::string_view getName() const { return _name; } @@ -102,7 +102,7 @@ namespace lms::db static pointer find(Session& session, LabelId id); static pointer find(Session& session, std::string_view name); static void find(Session& session, LabelSortMethod sortMethod, std::function func); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // Accessors std::string_view getName() const { return _name; } @@ -134,7 +134,7 @@ namespace lms::db static pointer find(Session& session, ReleaseTypeId id); static pointer find(Session& session, std::string_view name); static void find(Session& session, ReleaseTypeSortMethod sortMethod, std::function func); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // Accessors std::string_view getName() const { return _name; } @@ -170,8 +170,7 @@ namespace lms::db Wt::WDateTime writtenAfter; std::optional dateRange; std::optional originalDateRange; - UserId starringUser; // only releases starred by this user - std::optional feedbackBackend; // and for this backend + UserId starringUser; // only releases starred by this user (uses their current feedback backend) ArtistId artist; // only releases by this release artist ArtistId trackArtist; // only releases that involved this track artist core::EnumSet trackArtistLinkTypes; // and for these link types, if set @@ -220,10 +219,9 @@ namespace lms::db originalDateRange = _originalDateRange; return *this; } - FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) + FindParameters& setStarringUser(UserId _user) { starringUser = _user; - feedbackBackend = _feedbackBackend; return *this; } FindParameters& setArtist(ArtistId _artist) @@ -269,11 +267,11 @@ namespace lms::db static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function& func, MediaLibraryId library = {}); static void find(Session& session, const IdRange& idRange, const std::function& func); static IdRange findNextIdRange(Session& session, ReleaseId lastRetrievedId, std::size_t count); - static RangeResults find(Session& session, const FindParameters& parameters); + static std::vector find(Session& session, const FindParameters& parameters); static void find(Session& session, const FindParameters& parameters, const std::function& func); - static RangeResults findIds(Session& session, const FindParameters& parameters); + static std::vector findIds(Session& session, const FindParameters& parameters); static std::size_t getCount(Session& session, const FindParameters& parameters); - static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); // not track related + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); // not track related // Updates static void updatePreferredArtwork(Session& session, ReleaseId id, ArtworkId artworkId); diff --git a/src/libs/database/include/database/objects/ScanSettings.hpp b/src/libs/database/include/database/objects/ScanSettings.hpp index d8e68db1..e14aca6d 100644 --- a/src/libs/database/include/database/objects/ScanSettings.hpp +++ b/src/libs/database/include/database/objects/ScanSettings.hpp @@ -29,6 +29,7 @@ #include "database/IdType.hpp" #include "database/Object.hpp" +#include "database/objects/Types.hpp" LMS_DECLARE_IDTYPE(ScanSettingsId) @@ -49,14 +50,6 @@ namespace lms::db Hourly, }; - // Do not modify values (just add) - enum class RecommendationEngineType - { - Clusters = 0, - None = 2, - AudioSimilarity = 3, - }; - ScanSettings() = default; static pointer find(Session& session, std::string_view name = ""); diff --git a/src/libs/database/include/database/objects/ServerInfo.hpp b/src/libs/database/include/database/objects/ServerInfo.hpp new file mode 100644 index 00000000..8b1005c2 --- /dev/null +++ b/src/libs/database/include/database/objects/ServerInfo.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2026 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 "core/UUID.hpp" + +namespace lms::db +{ + class Session; + + // Singleton row holding server-level metadata (not tied to any particular schema version) + class ServerInfo + { + public: + using pointer = Wt::Dbo::ptr; + + ServerInfo() = default; + + static pointer getOrCreate(Session& session); + static pointer get(Session& session); + + core::UUID getInstanceId() const { return _instanceId; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _instanceId, "instance_id"); + } + + private: + explicit ServerInfo(core::UUID instanceId); + + core::UUID _instanceId; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/objects/StarredTrack.hpp b/src/libs/database/include/database/objects/StarredTrack.hpp index 9a6b1c58..f0dbb1ca 100644 --- a/src/libs/database/include/database/objects/StarredTrack.hpp +++ b/src/libs/database/include/database/objects/StarredTrack.hpp @@ -73,7 +73,7 @@ namespace lms::db static pointer find(Session& session, TrackId trackId, UserId userId); // current feedback backend static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend); static bool exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend); - static RangeResults find(Session& session, const FindParameters& findParams); + static std::vector find(Session& session, const FindParameters& findParams); // Accessors ObjectPtr getTrack() const { return _track; } diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index d9783b9f..ca743af6 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -51,12 +51,14 @@ #include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediumId.hpp" #include "database/objects/MoodId.hpp" +#include "database/objects/MovementId.hpp" #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackId.hpp" #include "database/objects/TrackListId.hpp" #include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" +#include "database/objects/WorkId.hpp" #include "database/objects/detail/Types.hpp" namespace lms::db @@ -69,16 +71,18 @@ namespace lms::db class Genre; class Grouping; class Language; + class Medium; + class Movement; class Mood; class TrackEmbeddedImageLink; class MediaLibrary; - class Medium; class Release; class Session; class TrackArtistLink; class TrackLyrics; class TrackStats; class User; + class Work; class Track final : public Object { @@ -91,8 +95,7 @@ namespace lms::db 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 + UserId starringUser; // only tracks starred by this user (uses their current feedback backend) ArtistId artist; // only tracks that involve this artist std::string artistName; // only tracks that involve this artist name core::EnumSet trackArtistLinkTypes; // and for these link types @@ -138,10 +141,9 @@ namespace lms::db writtenAfter = _after; return *this; } - FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) + FindParameters& setStarringUser(UserId _user) { starringUser = _user; - feedbackBackend = _feedbackBackend; return *this; } FindParameters& setArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}) @@ -231,12 +233,11 @@ namespace lms::db static bool exists(Session& session, TrackId id); static std::vector findByRecordingMBID(Session& session, const core::UUID& MBID); static std::vector findByMBID(Session& session, const core::UUID& MBID); - static RangeResults findIds(Session& session, const FindParameters& params); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector findIds(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, const std::function& func); - static void find(Session& session, const FindParameters& params, bool& moreResults, const std::function& func); static std::size_t getCount(Session& session, const FindParameters& params); - static RangeResults findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); + static std::vector findIdsTrackMBIDDuplicates(Session& session, std::optional range = std::nullopt); // Update utility functions static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId); @@ -282,6 +283,8 @@ namespace lms::db void setGroupings(std::span> groupings); void setLanguages(std::span> languages); void setMoods(std::span> moods); + void setWorks(std::span> works); + void clearMovements(); void clearLyrics(); void clearEmbeddedLyrics(); void addLyrics(const ObjectPtr& lyrics); @@ -313,7 +316,7 @@ namespace lms::db // Metadata std::optional getTrackNumber() const { return _trackNumber; } - std::string getName() const { return _name; } + std::string_view getName() const { return _name; } const core::PartialDateTime& getDate() const { return _date; } std::optional getYear() const; const core::PartialDateTime& getOriginalDate() const { return _originalDate; } @@ -350,6 +353,10 @@ namespace lms::db std::vector getLanguageIds() const; std::vector> getMoods() const; std::vector getMoodIds() const; + std::vector> getWorks() const; + bool hasWork() const; + std::vector> getMovements() const; + bool hasMovement() const; ObjectPtr getMediaLibrary() const; ObjectPtr getDirectory() const; ObjectPtr getPreferredArtwork() const; @@ -396,12 +403,14 @@ namespace lms::db Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull); + Wt::Dbo::hasMany(a, _movements, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _groupings, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _languages, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _moods, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _works, Wt::Dbo::ManyToMany, "track_work", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track"); } @@ -444,7 +453,6 @@ namespace lms::db std::string _artistDisplayName; std::string _comment; Advisory _advisory{ Advisory::UnSet }; - Wt::Dbo::ptr _medium; Wt::Dbo::ptr _release; Wt::Dbo::ptr _mediaLibrary; @@ -457,6 +465,8 @@ namespace lms::db Wt::Dbo::collection> _groupings; Wt::Dbo::collection> _languages; Wt::Dbo::collection> _moods; + Wt::Dbo::collection> _works; + Wt::Dbo::collection> _movements; Wt::Dbo::collection> _trackLyrics; Wt::Dbo::collection> _embeddedImageLinks; }; diff --git a/src/libs/database/include/database/objects/TrackBookmark.hpp b/src/libs/database/include/database/objects/TrackBookmark.hpp index 833747e8..062f3467 100644 --- a/src/libs/database/include/database/objects/TrackBookmark.hpp +++ b/src/libs/database/include/database/objects/TrackBookmark.hpp @@ -46,7 +46,7 @@ namespace lms::db // Find utility functions static std::size_t getCount(Session& session); static pointer find(Session& session, TrackBookmarkId id); - static RangeResults find(Session& session, UserId userId, std::optional range = std::nullopt); + static std::vector 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/database/include/database/objects/TrackEmbeddedImage.hpp b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp index 2c4bf49f..d9cbfeea 100644 --- a/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp +++ b/src/libs/database/include/database/objects/TrackEmbeddedImage.hpp @@ -100,7 +100,7 @@ namespace lms::db static pointer find(Session& session, TrackEmbeddedImageId id); static void find(Session& session, const FindParameters& params, const std::function& func); static pointer find(Session& session, std::size_t size, ImageHashType hash); - static RangeResults findOrphanIds(Session& session, std::optional range); + static std::vector findOrphanIds(Session& session, std::optional range); // getters ImageHashType getHash() const { return _hash; } diff --git a/src/libs/database/include/database/objects/TrackList.hpp b/src/libs/database/include/database/objects/TrackList.hpp index 14b2c412..685adfcc 100644 --- a/src/libs/database/include/database/objects/TrackList.hpp +++ b/src/libs/database/include/database/objects/TrackList.hpp @@ -116,7 +116,7 @@ namespace lms::db 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); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, const std::function& func); // Accessors @@ -139,7 +139,7 @@ namespace lms::db bool isEmpty() const; std::size_t getCount() const; ObjectPtr getEntry(std::size_t pos) const; - RangeResults> getEntries(std::optional range = {}) const; + std::vector> getEntries(std::optional range = {}) const; ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; std::vector getTrackIds() const; diff --git a/src/libs/database/include/database/objects/TrackLyrics.hpp b/src/libs/database/include/database/objects/TrackLyrics.hpp index ee7e35bf..e35c63bc 100644 --- a/src/libs/database/include/database/objects/TrackLyrics.hpp +++ b/src/libs/database/include/database/objects/TrackLyrics.hpp @@ -83,7 +83,7 @@ namespace lms::db static pointer find(Session& session, const std::filesystem::path& file); static void find(Session& session, const FindParameters& params, const std::function& func); static void find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function& func); - static RangeResults findOrphanIds(Session& session, std::optional range); + static std::vector findOrphanIds(Session& session, std::optional range); static void findAbsoluteFilePath(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function& func); using SynchronizedLines = std::map; diff --git a/src/libs/database/include/database/objects/TrackMusicNNEmbeddings.hpp b/src/libs/database/include/database/objects/TrackMusicNNEmbeddings.hpp index ea99bbcb..2403b803 100644 --- a/src/libs/database/include/database/objects/TrackMusicNNEmbeddings.hpp +++ b/src/libs/database/include/database/objects/TrackMusicNNEmbeddings.hpp @@ -45,7 +45,7 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer find(Session& session, TrackMusicNNEmbeddingsId id); static pointer find(Session& session, TrackId trackId); - static RangeResults find(Session& session, std::optional range = std::nullopt); + static std::vector find(Session& session, std::optional range = std::nullopt); static void find(Session& session, std::function func); static void removeAll(Session& session); diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp index 851e66b9..052f9b61 100644 --- a/src/libs/database/include/database/objects/Types.hpp +++ b/src/libs/database/include/database/objects/Types.hpp @@ -114,6 +114,13 @@ namespace lms::db PubDateDesc, }; + enum class RecommendationEngineType + { + Clusters = 0, + None = 2, + AudioSimilarity = 3, + }; + enum class ReleaseArtistLinkSortMethod { None, diff --git a/src/libs/database/include/database/objects/User.hpp b/src/libs/database/include/database/objects/User.hpp index 79244d51..0b21556b 100644 --- a/src/libs/database/include/database/objects/User.hpp +++ b/src/libs/database/include/database/objects/User.hpp @@ -88,7 +88,7 @@ namespace lms::db static std::size_t getCount(Session& session); static pointer find(Session& session, UserId id); static pointer find(Session& session, std::string_view loginName); - static RangeResults find(Session& session, const FindParameters& params); + static std::vector find(Session& session, const FindParameters& params); static void find(Session& session, const FindParameters& params, const std::function& func); static pointer findDemoUser(Session& session); diff --git a/src/libs/database/include/database/objects/Work.hpp b/src/libs/database/include/database/objects/Work.hpp new file mode 100644 index 00000000..cc7a3d00 --- /dev/null +++ b/src/libs/database/include/database/objects/Work.hpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2025 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 +#include + +#include +#include + +#include "core/UUID.hpp" + +#include "database/Object.hpp" +#include "database/Types.hpp" +#include "database/objects/ReleaseId.hpp" +#include "database/objects/TrackId.hpp" +#include "database/objects/WorkId.hpp" + +namespace lms::db +{ + class Session; + class Track; + + class Work final : public Object + { + public: + static constexpr std::size_t maxNameLength{ 512 }; + + Work() = default; + + static pointer find(Session& session, WorkId id); + // Global lookup: MusicBrainz Work Id is a strong, unambiguous identity shared across the whole library + static pointer find(Session& session, const core::UUID& mbid); + // Name-only lookup, scoped to works already linked to a track of the given release: work titles are + // often generic (e.g. "Symphony No. 5") and collide across unrelated works, so without an mbid we only + // ever match within the same release instead of matching globally by name + static pointer find(Session& session, ReleaseId releaseId, std::string_view name); + static std::vector findOrphanIds(Session& session, std::optional range = std::nullopt); + + void setName(std::string_view name); + + std::string_view getName() const { return _name; } + std::optional getMBID() const { return _mbid; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _name, "name"); + Wt::Dbo::field(a, _mbid, "mbid"); + Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_work", "", Wt::Dbo::OnDeleteCascade); + } + + private: + friend class Session; + Work(std::string_view name, const std::optional& mbid); + static pointer create(Session& session, std::string_view name, const std::optional& mbid); + + std::string _name; + std::optional _mbid; + Wt::Dbo::collection> _tracks; + }; + +} // namespace lms::db diff --git a/src/libs/database/include/database/objects/WorkId.hpp b/src/libs/database/include/database/objects/WorkId.hpp new file mode 100644 index 00000000..35e1ca3d --- /dev/null +++ b/src/libs/database/include/database/objects/WorkId.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 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 "database/IdType.hpp" + +LMS_DECLARE_IDTYPE(WorkId) diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 5a8f0e7f..c9271069 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -55,20 +55,20 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); artists = Artist::findOrphanIds(session); - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::find(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front()->getId(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist.getId()); } { @@ -181,10 +181,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 0); - ASSERT_FALSE(artists.moreResults); - ASSERT_EQ(artists.range.offset, 0); - ASSERT_EQ(artists.range.size, 0); + ASSERT_EQ(artists.size(), 0); } ScopedArtist artist1{ session, "MyArtist1" }; @@ -195,43 +192,31 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 3); - ASSERT_FALSE(artists.moreResults); - ASSERT_EQ(artists.range.offset, 0); - ASSERT_EQ(artists.range.size, 3); + ASSERT_EQ(artists.size(), 3); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 0, 1 })) }; - ASSERT_EQ(artists.results.size(), 1); - ASSERT_TRUE(artists.moreResults); - ASSERT_EQ(artists.range.offset, 0); - ASSERT_EQ(artists.range.size, 1); - EXPECT_EQ(artists.results[0], artist1.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist1.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 1, 1 })) }; - ASSERT_EQ(artists.results.size(), 1); - ASSERT_TRUE(artists.moreResults); - ASSERT_EQ(artists.range.offset, 1); - ASSERT_EQ(artists.range.size, 1); - EXPECT_EQ(artists.results[0], artist2.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist2.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 2, 1 })) }; - ASSERT_EQ(artists.results.size(), 1); - ASSERT_FALSE(artists.moreResults); - ASSERT_EQ(artists.range.offset, 2); - ASSERT_EQ(artists.range.size, 1); - EXPECT_EQ(artists.results[0], artist3.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist3.getId()); } } @@ -249,7 +234,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } { @@ -310,29 +295,29 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); } { auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).size(), 0); } } @@ -343,7 +328,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findOrphanIds(session).results, std::vector{ artist.getId() }); + EXPECT_EQ(Artist::findOrphanIds(session), std::vector{ artist.getId() }); } { @@ -353,7 +338,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } } @@ -374,19 +359,19 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } @@ -416,26 +401,26 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library1->getId()))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library2->getId()))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } @@ -460,15 +445,15 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { @@ -479,8 +464,8 @@ namespace lms::db::tests params.setFilters(Filters{}.setMediaLibrary(library.getId())); auto artists{ Artist::findIds(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } { @@ -491,7 +476,7 @@ namespace lms::db::tests params.setFilters(Filters{}.setMediaLibrary(library.getId())); auto artists{ Artist::findIds(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } { @@ -502,8 +487,8 @@ namespace lms::db::tests params.setFilters(Filters{}.setMediaLibrary(library.getId())); auto artists{ Artist::findIds(session, params) }; - EXPECT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + EXPECT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } } @@ -521,18 +506,18 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findOrphanIds(session, Range{}).results.size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session, Range{}).size(), 0); } { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Composer)).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Composer)).size(), 0); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).size(), 0); } { @@ -549,17 +534,17 @@ namespace lms::db::tests EXPECT_EQ(track->getArtistLinks().size(), 3); auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId())) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Producer })); - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })); - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Writer })); - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer })); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { @@ -609,7 +594,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } { @@ -623,32 +608,32 @@ namespace lms::db::tests EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Arranger }).size(), 0); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).size(), 2); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).size(), 2); } { auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId())) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track->getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track->getId()); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId())); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track->getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track->getId()); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Arranger })); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist })); - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Arranger })); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); } { @@ -697,7 +682,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).size(), 0); } { @@ -707,7 +692,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).size(), 1); } } @@ -734,8 +719,8 @@ namespace lms::db::tests params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC)); const auto artists{ Artist::find(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front()->getId(), artist2.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); } } @@ -753,15 +738,15 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).size(), 0); const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "A" })) }; - ASSERT_EQ(artistsByAAA.results.size(), 1); - EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); + ASSERT_EQ(artistsByAAA.size(), 1); + EXPECT_EQ(artistsByAAA.front(), artist.getId()); const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "Z" })) }; - ASSERT_EQ(artistsByZZZ.results.size(), 1); - EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); + ASSERT_EQ(artistsByZZZ.size(), 1); + EXPECT_EQ(artistsByZZZ.front(), artist.getId()); EXPECT_EQ(Artist::find(session, "NNN").size(), 0); EXPECT_EQ(Artist::find(session, "AAA").size(), 1); @@ -804,27 +789,27 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; { const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "MyArtist" })) }; - EXPECT_EQ(artists.results.size(), 6); + EXPECT_EQ(artists.size(), 6); } { const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "MyArtist%" }).setSortMethod(ArtistSortMethod::Name)) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist1.getId()); - EXPECT_EQ(artists.results[1], artist4.getId()); + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], artist1.getId()); + EXPECT_EQ(artists[1], artist4.getId()); } { const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "%MyArtist" }).setSortMethod(ArtistSortMethod::Name)) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist2.getId()); - EXPECT_EQ(artists.results[1], artist5.getId()); + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], artist2.getId()); + EXPECT_EQ(artists[1], artist5.getId()); } { const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "_MyArtist" }).setSortMethod(ArtistSortMethod::Name)) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist3.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist3.getId()); } } } @@ -934,13 +919,13 @@ namespace lms::db::tests auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Name)) }; auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName)) }; - ASSERT_EQ(allArtistsByName.results.size(), 2); - EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); - EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); + ASSERT_EQ(allArtistsByName.size(), 2); + EXPECT_EQ(allArtistsByName.front(), artistA.getId()); + EXPECT_EQ(allArtistsByName.back(), artistB.getId()); - ASSERT_EQ(allArtistsBySortName.results.size(), 2); - EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); - EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); + ASSERT_EQ(allArtistsBySortName.size(), 2); + EXPECT_EQ(allArtistsBySortName.front(), artistB.getId()); + EXPECT_EQ(allArtistsBySortName.back(), artistA.getId()); } } @@ -955,7 +940,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setNonRelease(true).setArtist(artist->getId())) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { @@ -971,8 +956,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId()).setNonRelease(true)) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track2.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track2.getId()); } } @@ -1033,11 +1018,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::AddedDesc)) }; - ASSERT_EQ(artists.results.size(), 4); - EXPECT_EQ(artists.results[0], artistD.getId()); - EXPECT_EQ(artists.results[1], artistA.getId()); - EXPECT_EQ(artists.results[2], artistB.getId()); - EXPECT_EQ(artists.results[3], artistC.getId()); + ASSERT_EQ(artists.size(), 4); + EXPECT_EQ(artists[0], artistD.getId()); + EXPECT_EQ(artists[1], artistA.getId()); + EXPECT_EQ(artists[2], artistB.getId()); + EXPECT_EQ(artists[3], artistC.getId()); } } @@ -1073,11 +1058,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::LastWrittenDesc)) }; - ASSERT_EQ(artists.results.size(), 4); - EXPECT_EQ(artists.results[0], artistA.getId()); - EXPECT_EQ(artists.results[1], artistD.getId()); - EXPECT_EQ(artists.results[2], artistB.getId()); - EXPECT_EQ(artists.results[3], artistC.getId()); + ASSERT_EQ(artists.size(), 4); + EXPECT_EQ(artists[0], artistA.getId()); + EXPECT_EQ(artists[1], artistD.getId()); + EXPECT_EQ(artists[2], artistB.getId()); + EXPECT_EQ(artists[3], artistC.getId()); } } @@ -1144,8 +1129,8 @@ namespace lms::db::tests ArtistId lastRetrievedArtist; const auto results{ Artist::findWithMBIDNameVariants(session, lastRetrievedArtist) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), artistA.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), artistA.getId()); EXPECT_EQ(lastRetrievedArtist, artistA.getId()); } } diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 90a204f1..0858c8b6 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -16,6 +16,7 @@ add_executable(test-database Medium.cpp Migration.cpp Mood.cpp + Movement.cpp PlayListFile.cpp Podcast.cpp RatedArtist.cpp @@ -29,6 +30,7 @@ add_executable(test-database StarredTrack.cpp Track.cpp TrackArtistLink.cpp + Work.cpp TrackBookmark.cpp TrackEmbeddedImage.cpp TrackList.cpp diff --git a/src/libs/database/test/Cluster.cpp b/src/libs/database/test/Cluster.cpp index 4936a6a5..1800c438 100644 --- a/src/libs/database/test/Cluster.cpp +++ b/src/libs/database/test/Cluster.cpp @@ -50,26 +50,26 @@ namespace lms::db::tests { const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), cluster.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), cluster.getId()); } { const auto clusters{ Cluster::findOrphanIds(session) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), cluster.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), cluster.getId()); } auto clusterTypes{ ClusterType::findIds(session) }; - ASSERT_EQ(clusterTypes.results.size(), 1); - EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + ASSERT_EQ(clusterTypes.size(), 1); + EXPECT_EQ(clusterTypes.front(), clusterType.getId()); clusterTypes = ClusterType::findUsed(session); - ASSERT_EQ(clusterTypes.results.size(), 1); - EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + ASSERT_EQ(clusterTypes.size(), 1); + EXPECT_EQ(clusterTypes.front(), clusterType.getId()); clusterTypes = ClusterType::findOrphanIds(session); - EXPECT_EQ(clusterTypes.results.size(), 0); + EXPECT_EQ(clusterTypes.size(), 0); } } @@ -77,10 +77,10 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto clusterTypes{ ClusterType::findOrphanIds(session) }; - ASSERT_EQ(clusterTypes.results.size(), 1); - EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + ASSERT_EQ(clusterTypes.size(), 1); + EXPECT_EQ(clusterTypes.front(), clusterType.getId()); - ASSERT_EQ(ClusterType::findUsed(session).results.size(), 0); + ASSERT_EQ(ClusterType::findUsed(session).size(), 0); } } @@ -155,10 +155,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); auto clusterTypes{ ClusterType::findOrphanIds(session) }; - ASSERT_EQ(clusterTypes.results.size(), 1); - EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + ASSERT_EQ(clusterTypes.size(), 1); + EXPECT_EQ(clusterTypes.front(), clusterType.getId()); } ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; @@ -167,7 +167,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto clusters{ Cluster::findOrphanIds(session) }; - EXPECT_EQ(clusters.results.size(), 2); + EXPECT_EQ(clusters.size(), 2); EXPECT_EQ(track->getClusters().size(), 0); EXPECT_EQ(track->getClusterIds().size(), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0); @@ -183,8 +183,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setTrack(track.getId())) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), cluster1.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), cluster1.getId()); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } @@ -192,21 +192,21 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto clusters{ Cluster::findOrphanIds(session) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), cluster2.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), cluster2.getId()); - EXPECT_EQ(ClusterType::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(ClusterType::findOrphanIds(session).size(), 0); } { auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId() }))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); tracks = Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster2.getId() }))); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { @@ -236,7 +236,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { @@ -249,7 +249,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); } @@ -264,8 +264,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1); } @@ -289,11 +289,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size()); - for (TrackId trackId : cluster->getTracks().results) + for (TrackId trackId : cluster->getTracks()) { auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) }; EXPECT_TRUE(it != std::cend(tracks)); @@ -333,8 +333,8 @@ namespace lms::db::tests { { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}).results.size(), 0); - EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.size(), 0); + EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}).size(), 0); + EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).size(), 0); } ScopedClusterType clusterType{ session, "MyClusterType" }; @@ -342,18 +342,18 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}).results }; + auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}) }; ASSERT_EQ(clusters.size(), 1); EXPECT_EQ(clusters.front(), cluster.getId()); - clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())).results; + clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())); ASSERT_EQ(clusters.size(), 1); EXPECT_EQ(clusters.front(), cluster.getId()); - clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results; + clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")); EXPECT_EQ(clusters.size(), 0); - clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")).results; + clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")); ASSERT_EQ(clusters.size(), 1); EXPECT_EQ(clusters.front(), cluster.getId()); } @@ -366,7 +366,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); } ScopedClusterType clusterType{ session, "MyClusterType" }; @@ -375,9 +375,9 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2); - EXPECT_EQ(Release::find(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ unusedCluster.getId() }))).results.size(), 0); - EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); + ASSERT_EQ(Cluster::findOrphanIds(session).size(), 2); + EXPECT_EQ(Release::find(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ unusedCluster.getId() }))).size(), 0); + EXPECT_EQ(Release::find(session, Release::FindParameters{}).size(), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); } @@ -394,8 +394,8 @@ namespace lms::db::tests { auto clusters{ Cluster::findOrphanIds(session) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), unusedCluster.getId()); } EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); @@ -405,23 +405,23 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) }; - ASSERT_EQ(clusters.results.size(), 1); - EXPECT_EQ(clusters.results.front(), cluster.getId()); + ASSERT_EQ(clusters.size(), 1); + EXPECT_EQ(clusters.front(), cluster.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ unusedCluster.getId() }))) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -451,10 +451,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(ClusterType::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2); - EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(ClusterType::findOrphanIds(session).size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 2); + EXPECT_EQ(Release::findOrphanIds(session).size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } { @@ -467,11 +467,11 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId() }))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster2.getId() }))).results.size(), 0); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster3.getId() }))).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster2.getId() }))).size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster3.getId() }))).size(), 0); cluster2.get().modify()->addTrack(track.get()); } @@ -480,18 +480,18 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId() }))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster2.getId() }))); - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId() }))); - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster3.getId() }))).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster3.getId() }))).size(), 0); } } @@ -512,17 +512,17 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); + EXPECT_EQ(Release::findOrphanIds(session).size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } } @@ -552,8 +552,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); } { @@ -563,8 +563,8 @@ namespace lms::db::tests std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); }); auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); } } @@ -587,26 +587,26 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(ClusterType::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); - EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0); + EXPECT_EQ(ClusterType::findOrphanIds(session).size(), 0); + EXPECT_EQ(Artist::findOrphanIds(session).size(), 0); + EXPECT_EQ(Release::findOrphanIds(session).size(), 0); } { auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front(), artist.getId()); auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } } @@ -632,12 +632,12 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId(), cluster2.getId() }))); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } } @@ -743,17 +743,17 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto genres{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GENRE")) }; - ASSERT_EQ(genres.results.size(), 2); + ASSERT_EQ(genres.size(), 2); const auto moods{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("MOOD")) }; - ASSERT_EQ(moods.results.size(), 1); - EXPECT_EQ(moods.results.front()->getId(), mood.getId()); + ASSERT_EQ(moods.size(), 1); + EXPECT_EQ(moods.front()->getId(), mood.getId()); const auto all{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId())) }; - EXPECT_EQ(all.results.size(), 3); + EXPECT_EQ(all.size(), 3); const auto none{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GROUPING")) }; - EXPECT_EQ(none.results.size(), 0); + EXPECT_EQ(none.size(), 0); } } diff --git a/src/libs/database/test/Common.cpp b/src/libs/database/test/Common.cpp index a7e3df7d..af3f22d1 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -74,26 +74,6 @@ namespace lms::db::tests session.fullAnalyze(); } - TEST_F(DatabaseFixture, Common_subRangeEmpty) - { - 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); - } - } - TEST_F(DatabaseFixture, Common_subRangeForeach) { struct TestCase @@ -155,55 +135,4 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Common_subRange) - { - 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{ 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); - } - } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/DatabaseTest.cpp b/src/libs/database/test/DatabaseTest.cpp index 99ac564a..ea8369bc 100644 --- a/src/libs/database/test/DatabaseTest.cpp +++ b/src/libs/database/test/DatabaseTest.cpp @@ -42,19 +42,19 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); - EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); + EXPECT_TRUE(Release::findOrphanIds(session).empty()); + EXPECT_TRUE(Artist::findOrphanIds(session).empty()); } { auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); const auto releaseTracks{ Track::find(session, Track::FindParameters{}.setRelease(release.getId())) }; - EXPECT_EQ(releaseTracks.results.size(), nbTracks); + EXPECT_EQ(releaseTracks.size(), nbTracks); } } @@ -75,8 +75,8 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); auto artists{ release->getTrackArtists() }; ASSERT_EQ(artists.size(), 1); @@ -88,7 +88,7 @@ namespace lms::db::tests { { auto transaction{ session.createReadTransaction() }; - EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty()); + EXPECT_TRUE(User::find(session, User::FindParameters{}).empty()); EXPECT_EQ(User::getCount(session), 0); } @@ -97,7 +97,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1); + EXPECT_EQ(User::find(session, User::FindParameters{}).size(), 1); EXPECT_EQ(User::getCount(session), 1); } } diff --git a/src/libs/database/test/Directory.cpp b/src/libs/database/test/Directory.cpp index 6246cd1d..ab4c573d 100644 --- a/src/libs/database/test/Directory.cpp +++ b/src/libs/database/test/Directory.cpp @@ -148,7 +148,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto directories{ Directory::findOrphanIds(session).results }; + const auto directories{ Directory::findOrphanIds(session) }; EXPECT_EQ(directories.size(), 2); } @@ -161,7 +161,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto directories{ Directory::findOrphanIds(session).results }; + const auto directories{ Directory::findOrphanIds(session) }; ASSERT_EQ(directories.size(), 1); EXPECT_EQ(directories.front(), child.getId()); } @@ -182,7 +182,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto directories{ Directory::findRootDirectories(session).results }; + const auto directories{ Directory::findRootDirectories(session) }; ASSERT_EQ(directories.size(), 2); EXPECT_EQ(directories[0]->getId(), parent1.getId()); EXPECT_EQ(directories[1]->getId(), parent2.getId()); @@ -209,7 +209,7 @@ namespace lms::db::tests Directory::FindParameters params; params.setWithNoTrack(true); - auto res{ Directory::find(session, params).results }; + auto res{ Directory::find(session, params) }; ASSERT_EQ(res.size(), 3); EXPECT_EQ(res[0]->getId(), parent.getId()); @@ -228,7 +228,7 @@ namespace lms::db::tests Directory::FindParameters params; params.setWithNoTrack(true); - auto res{ Directory::find(session, params).results }; + auto res{ Directory::find(session, params) }; ASSERT_EQ(res.size(), 2); EXPECT_EQ(res[0]->getId(), parent.getId()); EXPECT_EQ(res[1]->getId(), child1.getId()); @@ -247,7 +247,7 @@ namespace lms::db::tests Directory::FindParameters params; params.setKeywords({ "foo" }); - auto res{ Directory::find(session, params).results }; + auto res{ Directory::find(session, params) }; ASSERT_EQ(res.size(), 2); EXPECT_EQ(res[0]->getId(), child1.getId()); @@ -267,7 +267,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()).results }; + const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()) }; ASSERT_EQ(res.size(), 2); EXPECT_EQ(res[0], parent1.getId()); EXPECT_EQ(res[1], child1.getId()); @@ -283,7 +283,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()).results }; + const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()) }; EXPECT_EQ(res.size(), 0); } } diff --git a/src/libs/database/test/Genre.cpp b/src/libs/database/test/Genre.cpp index 034a3095..9dbd7826 100644 --- a/src/libs/database/test/Genre.cpp +++ b/src/libs/database/test/Genre.cpp @@ -65,8 +65,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto orphans{ Genre::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), genre.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), genre.getId()); } } @@ -78,7 +78,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 2); + EXPECT_EQ(Genre::findOrphanIds(session).size(), 2); EXPECT_EQ(track->getGenres().size(), 0); EXPECT_EQ(track->getGenreIds().size(), 0); EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 0); @@ -94,15 +94,15 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setTrack(track.getId())) }; - ASSERT_EQ(genres.results.size(), 1); - EXPECT_EQ(genres.results.front(), genre1.getId()); + ASSERT_EQ(genres.size(), 1); + EXPECT_EQ(genres.front(), genre1.getId()); EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1); EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0); const auto orphans{ Genre::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), genre2.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), genre2.getId()); const auto trackGenres{ track->getGenres() }; ASSERT_EQ(trackGenres.size(), 1); @@ -116,11 +116,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) }; - EXPECT_EQ(tracks2.results.size(), 0); + EXPECT_EQ(tracks2.size(), 0); } } @@ -139,7 +139,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1); EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 1); - EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Genre::findOrphanIds(session).size(), 0); const auto trackGenres{ track->getGenres() }; EXPECT_EQ(trackGenres.size(), 2); @@ -148,12 +148,12 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) }; - ASSERT_EQ(tracks2.results.size(), 1); - EXPECT_EQ(tracks2.results.front(), track.getId()); + ASSERT_EQ(tracks2.size(), 1); + EXPECT_EQ(tracks2.front(), track.getId()); } } @@ -200,10 +200,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::Name)) }; - ASSERT_EQ(genres.results.size(), 3); - EXPECT_EQ(genres.results[0], g2.getId()); - EXPECT_EQ(genres.results[1], g3.getId()); - EXPECT_EQ(genres.results[2], g1.getId()); + ASSERT_EQ(genres.size(), 3); + EXPECT_EQ(genres[0], g2.getId()); + EXPECT_EQ(genres[1], g3.getId()); + EXPECT_EQ(genres[2], g1.getId()); } } @@ -225,9 +225,9 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::TrackCountDesc)) }; - ASSERT_EQ(genres.results.size(), 2); - EXPECT_EQ(genres.results[0], g1.getId()); - EXPECT_EQ(genres.results[1], g2.getId()); + ASSERT_EQ(genres.size(), 2); + EXPECT_EQ(genres[0], g1.getId()); + EXPECT_EQ(genres[1], g2.getId()); } } } // namespace lms::db::tests diff --git a/src/libs/database/test/Grouping.cpp b/src/libs/database/test/Grouping.cpp index dcd92973..52b5d644 100644 --- a/src/libs/database/test/Grouping.cpp +++ b/src/libs/database/test/Grouping.cpp @@ -63,8 +63,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto orphans{ Grouping::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), grouping.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), grouping.getId()); } } @@ -76,7 +76,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 2); + EXPECT_EQ(Grouping::findOrphanIds(session).size(), 2); EXPECT_EQ(track->getGroupings().size(), 0); EXPECT_EQ(track->getGroupingIds().size(), 0); } @@ -90,12 +90,12 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setTrack(track.getId())) }; - ASSERT_EQ(groupings.results.size(), 1); - EXPECT_EQ(groupings.results.front(), grouping1.getId()); + ASSERT_EQ(groupings.size(), 1); + EXPECT_EQ(groupings.front(), grouping1.getId()); const auto orphans{ Grouping::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), grouping2.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), grouping2.getId()); const auto trackGroupings{ track->getGroupings() }; ASSERT_EQ(trackGroupings.size(), 1); @@ -109,11 +109,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) }; - EXPECT_EQ(tracks2.results.size(), 0); + EXPECT_EQ(tracks2.size(), 0); } } @@ -130,7 +130,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Grouping::findOrphanIds(session).size(), 0); const auto trackGroupings{ track->getGroupings() }; EXPECT_EQ(trackGroupings.size(), 2); @@ -139,12 +139,12 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) }; - ASSERT_EQ(tracks2.results.size(), 1); - EXPECT_EQ(tracks2.results.front(), track.getId()); + ASSERT_EQ(tracks2.size(), 1); + EXPECT_EQ(tracks2.front(), track.getId()); } } @@ -157,10 +157,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::Name)) }; - ASSERT_EQ(groupings.results.size(), 3); - EXPECT_EQ(groupings.results[0], g2.getId()); - EXPECT_EQ(groupings.results[1], g3.getId()); - EXPECT_EQ(groupings.results[2], g1.getId()); + ASSERT_EQ(groupings.size(), 3); + EXPECT_EQ(groupings[0], g2.getId()); + EXPECT_EQ(groupings[1], g3.getId()); + EXPECT_EQ(groupings[2], g1.getId()); } } @@ -182,9 +182,9 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::TrackCountDesc)) }; - ASSERT_EQ(groupings.results.size(), 2); - EXPECT_EQ(groupings.results[0], g1.getId()); - EXPECT_EQ(groupings.results[1], g2.getId()); + ASSERT_EQ(groupings.size(), 2); + EXPECT_EQ(groupings[0], g1.getId()); + EXPECT_EQ(groupings[1], g2.getId()); } } } // namespace lms::db::tests diff --git a/src/libs/database/test/Image.cpp b/src/libs/database/test/Image.cpp index 129e1dc2..f52a3134 100644 --- a/src/libs/database/test/Image.cpp +++ b/src/libs/database/test/Image.cpp @@ -83,7 +83,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())).results.size(), 0); + EXPECT_EQ(Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())).size(), 0); } { @@ -93,7 +93,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto results{ Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())).results }; + const auto results{ Image::find(session, Image::FindParameters{}.setDirectory(directory.getId())) }; ASSERT_EQ(results.size(), 1); EXPECT_EQ(results.front()->getId(), image.getId()); } @@ -138,8 +138,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem(fileStem); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -147,8 +147,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem(fileStem, Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -156,7 +156,7 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("nonexistent"); const auto results{ Image::find(session, params) }; - EXPECT_EQ(results.results.size(), 0); + EXPECT_EQ(results.size(), 0); } { @@ -164,7 +164,7 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("ima*"); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 0); + ASSERT_EQ(results.size(), 0); } { @@ -172,8 +172,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("ima*", Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -181,8 +181,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("*ge", Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -190,8 +190,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("*g*", Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -199,8 +199,8 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("*", Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results[0]->getId(), image.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results[0]->getId(), image.getId()); } { @@ -208,7 +208,7 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("ima%"); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 0); + ASSERT_EQ(results.size(), 0); } { @@ -216,7 +216,7 @@ namespace lms::db::tests Image::FindParameters params; params.setFileStem("ima%", Image::FindParameters::ProcessWildcards{ true }); const auto results{ Image::find(session, params) }; - ASSERT_EQ(results.results.size(), 0); + ASSERT_EQ(results.size(), 0); } } diff --git a/src/libs/database/test/Language.cpp b/src/libs/database/test/Language.cpp index dcf73d96..0139968f 100644 --- a/src/libs/database/test/Language.cpp +++ b/src/libs/database/test/Language.cpp @@ -63,8 +63,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto orphans{ Language::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), language.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), language.getId()); } } @@ -76,7 +76,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Language::findOrphanIds(session).results.size(), 2); + EXPECT_EQ(Language::findOrphanIds(session).size(), 2); EXPECT_EQ(track->getLanguages().size(), 0); EXPECT_EQ(track->getLanguageIds().size(), 0); } @@ -90,12 +90,12 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setTrack(track.getId())) }; - ASSERT_EQ(languages.results.size(), 1); - EXPECT_EQ(languages.results.front(), language1.getId()); + ASSERT_EQ(languages.size(), 1); + EXPECT_EQ(languages.front(), language1.getId()); const auto orphans{ Language::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), language2.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), language2.getId()); const auto trackLanguages{ track->getLanguages() }; ASSERT_EQ(trackLanguages.size(), 1); @@ -109,11 +109,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) }; - EXPECT_EQ(tracks2.results.size(), 0); + EXPECT_EQ(tracks2.size(), 0); } } @@ -130,7 +130,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Language::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Language::findOrphanIds(session).size(), 0); const auto trackLanguages{ track->getLanguages() }; EXPECT_EQ(trackLanguages.size(), 2); @@ -139,12 +139,12 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) }; - ASSERT_EQ(tracks2.results.size(), 1); - EXPECT_EQ(tracks2.results.front(), track.getId()); + ASSERT_EQ(tracks2.size(), 1); + EXPECT_EQ(tracks2.front(), track.getId()); } } @@ -157,10 +157,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::Name)) }; - ASSERT_EQ(languages.results.size(), 3); - EXPECT_EQ(languages.results[0], l2.getId()); - EXPECT_EQ(languages.results[1], l3.getId()); - EXPECT_EQ(languages.results[2], l1.getId()); + ASSERT_EQ(languages.size(), 3); + EXPECT_EQ(languages[0], l2.getId()); + EXPECT_EQ(languages[1], l3.getId()); + EXPECT_EQ(languages[2], l1.getId()); } } @@ -182,9 +182,9 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::TrackCountDesc)) }; - ASSERT_EQ(languages.results.size(), 2); - EXPECT_EQ(languages.results[0], l1.getId()); - EXPECT_EQ(languages.results[1], l2.getId()); + ASSERT_EQ(languages.size(), 2); + EXPECT_EQ(languages[0], l1.getId()); + EXPECT_EQ(languages[1], l2.getId()); } } } // namespace lms::db::tests diff --git a/src/libs/database/test/Listen.cpp b/src/libs/database/test/Listen.cpp index 2b7dfbdb..bbe503b2 100644 --- a/src/libs/database/test/Listen.cpp +++ b/src/libs/database/test/Listen.cpp @@ -69,7 +69,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::ListenBrainz)) }; - EXPECT_EQ(listens.results.size(), 0); + EXPECT_EQ(listens.size(), 0); } { @@ -77,18 +77,17 @@ namespace lms::db::tests { auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) }; - EXPECT_EQ(listens.moreResults, false); - ASSERT_EQ(listens.results.size(), 1); - EXPECT_EQ(listens.results.front(), listen->getId()); + ASSERT_EQ(listens.size(), 1); + EXPECT_EQ(listens.front(), listen->getId()); } { auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::PendingAdd)) }; - EXPECT_EQ(listens.results.size(), 1); + EXPECT_EQ(listens.size(), 1); } { auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::Synchronized)) }; - EXPECT_EQ(listens.results.size(), 0); + EXPECT_EQ(listens.size(), 0); } } } @@ -105,10 +104,10 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) }; - ASSERT_EQ(listens.results.size(), 3); - EXPECT_EQ(listens.results[0], listen1.getId()); - EXPECT_EQ(listens.results[1], listen2.getId()); - EXPECT_EQ(listens.results[2], listen3.getId()); + ASSERT_EQ(listens.size(), 3); + EXPECT_EQ(listens[0], listen1.getId()); + EXPECT_EQ(listens[1], listen2.getId()); + EXPECT_EQ(listens[2], listen3.getId()); } } @@ -157,11 +156,9 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); const auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); - EXPECT_EQ(artists.moreResults, false); + EXPECT_EQ(artists.size(), 0); } ScopedTrack track2{ session }; @@ -179,32 +176,37 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist1->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist1->getId()); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 0); + ASSERT_EQ(artists.size(), 0); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); params.setTrackArtistLinkType(TrackArtistLinkType::Producer); auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::Internal); } { @@ -216,11 +218,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster->getId() }); auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } } @@ -245,10 +246,9 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; @@ -257,11 +257,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist1->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist1->getId()); } ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; @@ -271,25 +270,22 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist2->getId()); - EXPECT_EQ(artists.results[1], artist1->getId()); + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], artist2->getId()); + EXPECT_EQ(artists[1], artist1->getId()); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setRange(Range{ 0, 1 }); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.moreResults, true); - EXPECT_EQ(artists.results[0], artist2->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist2->getId()); } } @@ -313,11 +309,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 0); + ASSERT_EQ(artists.size(), 0); } { auto transaction{ session.createWriteTransaction() }; @@ -328,12 +323,11 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist.getId()); } } @@ -352,12 +346,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user.getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); const auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); - EXPECT_EQ(artists.moreResults, false); + EXPECT_EQ(artists.size(), 0); } { @@ -372,34 +364,31 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user.getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user.getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); auto artists{ Listen::getTopArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user.getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(otherLibrary.getId()); auto artists{ Listen::getTopArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } @@ -419,11 +408,9 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -433,23 +420,23 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -474,12 +461,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release1.getId()); } ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -488,13 +473,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release2.getId()); - EXPECT_EQ(releases.results[1], release1.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release2.getId()); + EXPECT_EQ(releases[1], release1.getId()); } ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -503,13 +486,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release1.getId()); - EXPECT_EQ(releases.results[1], release2.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release1.getId()); + EXPECT_EQ(releases[1], release2.getId()); } } @@ -533,11 +514,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { auto transaction{ session.createWriteTransaction() }; @@ -549,12 +529,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + EXPECT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); } } @@ -578,12 +557,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -593,25 +570,21 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(otherLibrary.getId()); auto releases{ Listen::getTopReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -626,11 +599,9 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 0); + ASSERT_EQ(tracks.size(), 0); } ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -640,23 +611,23 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -672,12 +643,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setArtist(artist->getId()); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 0); + ASSERT_EQ(tracks.size(), 0); } ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -687,11 +656,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setArtist(artist->getId()); auto tracks{ Listen::getTopTracks(session, params) }; - ASSERT_EQ(tracks.results.size(), 0); + ASSERT_EQ(tracks.size(), 0); } { @@ -704,13 +672,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setArtist(artist->getId()); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); } } @@ -727,12 +693,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track1.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track1.getId()); } ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -741,13 +705,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2.getId()); - EXPECT_EQ(tracks.results[1], track1.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track2.getId()); + EXPECT_EQ(tracks[1], track1.getId()); } ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; @@ -756,13 +718,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track1.getId()); - EXPECT_EQ(tracks.results[1], track2.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track1.getId()); + EXPECT_EQ(tracks[1], track2.getId()); } } @@ -780,11 +740,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { auto transaction{ session.createWriteTransaction() }; @@ -796,12 +755,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + EXPECT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); } } @@ -818,12 +776,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user.getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 0); + ASSERT_EQ(tracks.size(), 0); } { @@ -838,25 +794,21 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(library.getId()); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setMediaLibrary(otherLibrary.getId()); auto tracks{ Listen::getTopTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -878,12 +830,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setReleaseArtistsOnly(true); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); - EXPECT_EQ(artists.moreResults, false); + EXPECT_EQ(artists.size(), 0); } const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; @@ -894,12 +844,11 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setReleaseArtistsOnly(true); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); } } @@ -919,11 +868,9 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); - EXPECT_EQ(artists.moreResults, false); + EXPECT_EQ(artists.size(), 0); } const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; @@ -934,11 +881,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); } { @@ -946,34 +892,38 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setReleaseArtistsOnly(true); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); - EXPECT_EQ(artists.moreResults, false); + EXPECT_EQ(artists.size(), 0); } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); + } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 0); + ASSERT_EQ(artists.size(), 0); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::Internal); } { auto transaction{ session.createReadTransaction() }; Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setTrackArtistLinkType(TrackArtistLinkType::Producer); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } { ScopedClusterType clusterType{ session, "MyType" }; @@ -984,11 +934,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster->getId() }); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } } @@ -1013,10 +962,9 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; @@ -1025,11 +973,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist1->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist1->getId()); } ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; { @@ -1037,12 +984,11 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], artist2->getId()); - EXPECT_EQ(artists.results[1], artist1->getId()); + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], artist2->getId()); + EXPECT_EQ(artists[1], artist1->getId()); } ScopedListen listen3{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; { @@ -1050,13 +996,11 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.setRange(Range{ 0, 1 }); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.moreResults, true); - EXPECT_EQ(artists.results[0], artist2->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist2->getId()); } } @@ -1080,11 +1024,10 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 0); + ASSERT_EQ(artists.size(), 0); } { auto transaction{ session.createWriteTransaction() }; @@ -1095,12 +1038,11 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist.getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist.getId()); } } @@ -1128,8 +1070,8 @@ namespace lms::db::tests params.filters.setMediaLibrary(library.getId()); auto artists{ Listen::getRecentArtists(session, params) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results[0], artist->getId()); + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); } { auto transaction{ session.createReadTransaction() }; @@ -1138,7 +1080,7 @@ namespace lms::db::tests params.filters.setMediaLibrary(otherLibrary.getId()); auto artists{ Listen::getRecentArtists(session, params) }; - EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.size(), 0); } } @@ -1158,11 +1100,9 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 0); + ASSERT_EQ(releases.size(), 0); } const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; @@ -1173,23 +1113,23 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); + } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -1324,12 +1264,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release2.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release2.getId()); } ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; @@ -1338,13 +1276,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release1.getId()); - EXPECT_EQ(releases.results[1], release2.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release1.getId()); + EXPECT_EQ(releases[1], release2.getId()); } ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; @@ -1353,13 +1289,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release2.getId()); - EXPECT_EQ(releases.results[1], release1.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release2.getId()); + EXPECT_EQ(releases[1], release1.getId()); } ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; @@ -1368,13 +1302,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release2.getId()); - EXPECT_EQ(releases.results[1], release1.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release2.getId()); + EXPECT_EQ(releases[1], release1.getId()); } } @@ -1395,11 +1327,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; @@ -1410,11 +1341,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -1426,12 +1356,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + EXPECT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); } } @@ -1459,9 +1388,8 @@ namespace lms::db::tests params.filters.setMediaLibrary(library.getId()); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release.getId()); } { auto transaction{ session.createReadTransaction() }; @@ -1470,8 +1398,7 @@ namespace lms::db::tests params.filters.setMediaLibrary(otherLibrary.getId()); auto releases{ Listen::getRecentReleases(session, params) }; - EXPECT_EQ(releases.moreResults, false); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -1485,11 +1412,9 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 0); + ASSERT_EQ(tracks.size(), 0); } const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; @@ -1500,24 +1425,24 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); } + { + auto transaction{ session.createWriteTransaction() }; + user.lockAndGet().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz); + } { auto transaction{ session.createReadTransaction() }; Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -1543,9 +1468,8 @@ namespace lms::db::tests params.filters.setMediaLibrary(library.getId()); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); } { @@ -1555,8 +1479,7 @@ namespace lms::db::tests params.filters.setMediaLibrary(otherLibrary.getId()); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -1701,12 +1624,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track2.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track2.getId()); } ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; @@ -1715,13 +1636,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track1.getId()); - EXPECT_EQ(tracks.results[1], track2.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track1.getId()); + EXPECT_EQ(tracks[1], track2.getId()); } ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; @@ -1730,13 +1649,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2.getId()); - EXPECT_EQ(tracks.results[1], track1.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track2.getId()); + EXPECT_EQ(tracks[1], track1.getId()); } ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; @@ -1745,13 +1662,11 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.moreResults, false); - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2.getId()); - EXPECT_EQ(tracks.results[1], track1.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track2.getId()); + EXPECT_EQ(tracks[1], track1.getId()); } } @@ -1820,11 +1735,10 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { auto transaction{ session.createWriteTransaction() }; @@ -1836,12 +1750,161 @@ namespace lms::db::tests Listen::StatsFindParameters params; params.setUser(user->getId()); - params.setScrobblingBackend(ScrobblingBackend::Internal); params.filters.setClusters(std::initializer_list{ cluster.getId() }); auto tracks{ Listen::getRecentTracks(session, params) }; - EXPECT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0], track.getId()); + EXPECT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track.getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getTopArtists_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedArtist artist{ session, "MyArtist" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::ArtistStatsFindParameters params; + params.setUser(user->getId()); + Listen::getTopArtists(session, params, [&](const Artist::pointer& a) { + ids.push_back(a->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], artist->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getTopReleases_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedRelease release{ session, "MyRelease" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::StatsFindParameters params; + params.setUser(user->getId()); + Listen::getTopReleases(session, params, [&](const Release::pointer& r) { + ids.push_back(r->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], release->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getTopTracks_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::StatsFindParameters params; + params.setUser(user->getId()); + Listen::getTopTracks(session, params, [&](const Track::pointer& t) { + ids.push_back(t->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], track->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getRecentArtists_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedArtist artist{ session, "MyArtist" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::ArtistStatsFindParameters params; + params.setUser(user->getId()); + Listen::getRecentArtists(session, params, [&](const Artist::pointer& a) { + ids.push_back(a->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], artist->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getRecentReleases_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedRelease release{ session, "MyRelease" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::StatsFindParameters params; + params.setUser(user->getId()); + Listen::getRecentReleases(session, params, [&](const Release::pointer& r) { + ids.push_back(r->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], release->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getRecentTracks_callback) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createReadTransaction() }; + + std::vector ids; + Listen::StatsFindParameters params; + params.setUser(user->getId()); + Listen::getRecentTracks(session, params, [&](const Track::pointer& t) { + ids.push_back(t->getId()); + }); + ASSERT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], track->getId()); } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 90815466..d1484076 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.cpp @@ -37,6 +37,7 @@ #include "database/objects/RatedTrack.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ScanSettings.hpp" +#include "database/objects/ServerInfo.hpp" #include "database/objects/StarredArtist.hpp" #include "database/objects/StarredRelease.hpp" #include "database/objects/StarredTrack.hpp" @@ -344,6 +345,8 @@ VALUES // Now perform full migration db.getTLSSession().migrateSchemaIfNeeded(); + db.getTLSSession().createScanSettingsIfNeeded(); + db.getTLSSession().createServerInfoIfNeeded(); // Now perform some dummy finds to ensure all fields are correctly mapped { @@ -373,6 +376,7 @@ VALUES EXPECT_FALSE(ReleaseArtistLink::find(session, ReleaseArtistLinkId{})); EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{})); EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{})); + EXPECT_NE(ServerInfo::get(session)->getInstanceId(), core::UUID{}); EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{})); EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{})); EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{})); diff --git a/src/libs/database/test/Mood.cpp b/src/libs/database/test/Mood.cpp index b3545237..bb0d1ecb 100644 --- a/src/libs/database/test/Mood.cpp +++ b/src/libs/database/test/Mood.cpp @@ -63,8 +63,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto orphans{ Mood::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), mood.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), mood.getId()); } } @@ -76,7 +76,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 2); + EXPECT_EQ(Mood::findOrphanIds(session).size(), 2); EXPECT_EQ(track->getMoods().size(), 0); EXPECT_EQ(track->getMoodIds().size(), 0); } @@ -90,12 +90,12 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setTrack(track.getId())) }; - ASSERT_EQ(moods.results.size(), 1); - EXPECT_EQ(moods.results.front(), mood1.getId()); + ASSERT_EQ(moods.size(), 1); + EXPECT_EQ(moods.front(), mood1.getId()); const auto orphans{ Mood::findOrphanIds(session) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results.front(), mood2.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), mood2.getId()); const auto trackMoods{ track->getMoods() }; ASSERT_EQ(trackMoods.size(), 1); @@ -109,11 +109,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) }; - EXPECT_EQ(tracks2.results.size(), 0); + EXPECT_EQ(tracks2.size(), 0); } } @@ -130,7 +130,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Mood::findOrphanIds(session).size(), 0); const auto trackMoods{ track->getMoods() }; EXPECT_EQ(trackMoods.size(), 2); @@ -139,12 +139,12 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) }; - ASSERT_EQ(tracks2.results.size(), 1); - EXPECT_EQ(tracks2.results.front(), track.getId()); + ASSERT_EQ(tracks2.size(), 1); + EXPECT_EQ(tracks2.front(), track.getId()); } } @@ -157,10 +157,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::Name)) }; - ASSERT_EQ(moods.results.size(), 3); - EXPECT_EQ(moods.results[0], m2.getId()); - EXPECT_EQ(moods.results[1], m3.getId()); - EXPECT_EQ(moods.results[2], m1.getId()); + ASSERT_EQ(moods.size(), 3); + EXPECT_EQ(moods[0], m2.getId()); + EXPECT_EQ(moods[1], m3.getId()); + EXPECT_EQ(moods[2], m1.getId()); } } @@ -182,9 +182,9 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::TrackCountDesc)) }; - ASSERT_EQ(moods.results.size(), 2); - EXPECT_EQ(moods.results[0], m1.getId()); - EXPECT_EQ(moods.results[1], m2.getId()); + ASSERT_EQ(moods.size(), 2); + EXPECT_EQ(moods[0], m1.getId()); + EXPECT_EQ(moods[1], m2.getId()); } } } // namespace lms::db::tests diff --git a/src/libs/database/test/Movement.cpp b/src/libs/database/test/Movement.cpp new file mode 100644 index 00000000..84ec1269 --- /dev/null +++ b/src/libs/database/test/Movement.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 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 "Common.hpp" + +#include "database/objects/Movement.hpp" +#include "database/objects/Track.hpp" + +namespace lms::db::tests +{ + TEST_F(DatabaseFixture, Movement_create) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "Allegro con brio", std::size_t{ 1 }, std::size_t{ 4 }, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto movements{ track->getMovements() }; + ASSERT_EQ(movements.size(), 1); + ASSERT_FALSE(movements[0]->getName().empty()); + EXPECT_EQ(movements[0]->getName(), "Allegro con brio"); + ASSERT_TRUE(movements[0]->getNumber()); + EXPECT_EQ(*movements[0]->getNumber(), std::size_t{ 1 }); + ASSERT_TRUE(movements[0]->getCount()); + EXPECT_EQ(*movements[0]->getCount(), std::size_t{ 4 }); + } + } + + TEST_F(DatabaseFixture, Movement_createWithNullFields) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "", std::nullopt, std::nullopt, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto movements{ track->getMovements() }; + ASSERT_EQ(movements.size(), 1); + EXPECT_TRUE(movements[0]->getName().empty()); + EXPECT_FALSE(movements[0]->getNumber()); + EXPECT_FALSE(movements[0]->getCount()); + } + } + + TEST_F(DatabaseFixture, Movement_multipleMovementsOnTrack) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "Allegro con brio", std::size_t{ 1 }, std::size_t{ 4 }, track.get()); + Movement::create(session, "Andante con moto", std::size_t{ 2 }, std::size_t{ 4 }, track.get()); + Movement::create(session, "Scherzo. Allegro", std::size_t{ 3 }, std::size_t{ 4 }, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto movements{ track->getMovements() }; + EXPECT_EQ(movements.size(), 3); + } + } + + TEST_F(DatabaseFixture, Movement_cascadeDeleteWithTrack) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "Allegro", std::size_t{ 1 }, std::nullopt, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getMovements().size(), 1); + } + + // track goes out of scope here — movements are cascade deleted + } + + TEST_F(DatabaseFixture, Movement_clearMovements) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "Allegro", std::size_t{ 1 }, std::size_t{ 2 }, track.get()); + Movement::create(session, "Andante", std::size_t{ 2 }, std::size_t{ 2 }, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getMovements().size(), 2); + } + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->clearMovements(); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getMovements().size(), 0); + } + } + + TEST_F(DatabaseFixture, Movement_replacingMovements) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "OldMovement", std::size_t{ 1 }, std::nullopt, track.get()); + } + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->clearMovements(); + Movement::create(session, "NewMovement", std::size_t{ 1 }, std::nullopt, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto movements{ track->getMovements() }; + ASSERT_EQ(movements.size(), 1); + ASSERT_FALSE(movements[0]->getName().empty()); + EXPECT_EQ(movements[0]->getName(), "NewMovement"); + } + } + + TEST_F(DatabaseFixture, Movement_getNumberReturnsCorrectSizeT) + { + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + Movement::create(session, "", std::size_t{ 42 }, std::size_t{ 100 }, track.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + const auto movements{ track->getMovements() }; + ASSERT_EQ(movements.size(), 1); + ASSERT_TRUE(movements[0]->getNumber()); + EXPECT_EQ(*movements[0]->getNumber(), std::size_t{ 42 }); + ASSERT_TRUE(movements[0]->getCount()); + EXPECT_EQ(*movements[0]->getCount(), std::size_t{ 100 }); + } + } +} // namespace lms::db::tests diff --git a/src/libs/database/test/RatedArtist.cpp b/src/libs/database/test/RatedArtist.cpp index 38ddacdc..43e5b826 100644 --- a/src/libs/database/test/RatedArtist.cpp +++ b/src/libs/database/test/RatedArtist.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(RatedArtist::getCount(session), 0); auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - EXPECT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.size(), 1); } ScopedRatedArtist ratedArtist{ session, artist.lockAndGet(), user.lockAndGet() }; diff --git a/src/libs/database/test/RatedRelease.cpp b/src/libs/database/test/RatedRelease.cpp index ee4c66e0..5ff6bb31 100644 --- a/src/libs/database/test/RatedRelease.cpp +++ b/src/libs/database/test/RatedRelease.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(RatedRelease::getCount(session), 0); auto releases{ Release::findIds(session, Release::FindParameters{}) }; - EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.size(), 1); } ScopedRatedRelease ratedRelease{ session, release.lockAndGet(), user.lockAndGet() }; diff --git a/src/libs/database/test/RatedTrack.cpp b/src/libs/database/test/RatedTrack.cpp index 13ed38b5..a68360c3 100644 --- a/src/libs/database/test/RatedTrack.cpp +++ b/src/libs/database/test/RatedTrack.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(RatedTrack::getCount(session), 0); auto tracks{ Track::findIds(session, Track::FindParameters{}) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); } ScopedRatedTrack ratedTrack{ session, track.lockAndGet(), user.lockAndGet() }; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 9f5e3932..1378bc9f 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -57,21 +57,21 @@ namespace lms::db::tests { const auto releases{ Release::findOrphanIds(session) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } { const auto releases{ Release::findIds(session, Release::FindParameters{}) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); EXPECT_EQ(release->getDuration(), std::chrono::seconds{ 0 }); } { const auto releases{ Release::find(session, Release::FindParameters{}) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front()->getId(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release.getId()); } { @@ -280,11 +280,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); + EXPECT_EQ(Release::findOrphanIds(session).size(), 0); const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release.getId())) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); } { @@ -297,18 +297,18 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); } { auto transaction{ session.createWriteTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { auto transaction{ session.createWriteTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -316,11 +316,11 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release.getId())) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); auto releases{ Release::findOrphanIds(session) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } } @@ -340,13 +340,13 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); } { auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -383,39 +383,105 @@ namespace lms::db::tests { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Release" })) }; - EXPECT_EQ(releases.results.size(), 6); + EXPECT_EQ(releases.size(), 6); } { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "MyRelease" })) }; - ASSERT_EQ(releases.results.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); })); + ASSERT_EQ(releases.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); })); } { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "MyRelease%" })) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release2.getId()); - EXPECT_EQ(releases.results[1], release4.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release2.getId()); + EXPECT_EQ(releases[1], release4.getId()); } { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "%MyRelease" })) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], release3.getId()); - EXPECT_EQ(releases.results[1], release5.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], release3.getId()); + EXPECT_EQ(releases[1], release5.getId()); } { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Foo%MyRelease" })) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release5.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release5.getId()); } { const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "MyRelease%Foo" })) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0], release4.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release4.getId()); } } } + TEST_F(DatabaseFixture, ReleaseSearchByMediumName) + { + ScopedRelease release{ session, "The Beatles In Mono" }; + ScopedMedium medium{ session, release.lockAndGet() }; + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + track.get().modify()->setRelease(release.get()); + track.get().modify()->setMedium(medium.get()); + medium.get().modify()->setName("Sgt. Pepper's Lonely Hearts Club Band"); + } + + { + auto transaction{ session.createReadTransaction() }; + + // matches only the medium (discsubtitle), not the release name + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Sgt. Pepper" })) }; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); + } + // release name search still works + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Beatles In Mono" })) }; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); + } + // no match on either field + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "NoSuchKeyword" })) }; + EXPECT_EQ(releases.size(), 0); + } + } + } + + TEST_F(DatabaseFixture, ReleaseSearchByMediumName_multipleMediaNoDuplicates) + { + ScopedRelease release{ session, "The Beatles In Mono" }; + ScopedMedium medium1{ session, release.lockAndGet() }; + ScopedMedium medium2{ session, release.lockAndGet() }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + track1.get().modify()->setRelease(release.get()); + track1.get().modify()->setMedium(medium1.get()); + medium1.get().modify()->setName("Sgt. Pepper's Lonely Hearts Club Band"); + + track2.get().modify()->setRelease(release.get()); + track2.get().modify()->setMedium(medium2.get()); + medium2.get().modify()->setName("Abbey Road"); + } + + { + auto transaction{ session.createReadTransaction() }; + + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Sgt. Pepper" })) }; + ASSERT_EQ(releases.size(), 1); // not duplicated despite the 1:N join + EXPECT_EQ(releases.front(), release.getId()); + } + } + TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack) { ScopedRelease release1{ session, "MyRelease" }; @@ -508,8 +574,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.size(), 0); - EXPECT_EQ(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.size(), 0); + EXPECT_EQ(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).size(), 0); + EXPECT_EQ(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).size(), 0); } { @@ -538,16 +604,16 @@ namespace lms::db::tests { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track1A.getId()); - EXPECT_EQ(tracks.results[1], track1B.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track1A.getId()); + EXPECT_EQ(tracks[1], track1B.getId()); } { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2A.getId()); - EXPECT_EQ(tracks.results[1], track2B.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track2A.getId()); + EXPECT_EQ(tracks[1], track2B.getId()); } } } @@ -568,7 +634,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ -3000, 3000 })) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -595,15 +661,15 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); - ASSERT_EQ(releases.results.size(), 0); + ASSERT_EQ(releases.size(), 0); } } @@ -624,7 +690,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setOriginalDateRange(YearRange{ -3000, 3000 })) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -649,15 +715,15 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases = Release::findIds(session, Release::FindParameters{}.setOriginalDateRange(YearRange{ 1950, 2000 })); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setOriginalDateRange(YearRange{ 1993, 1993 })); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setOriginalDateRange(YearRange{ 1994, 1994 })); - ASSERT_EQ(releases.results.size(), 0); + ASSERT_EQ(releases.size(), 0); } } @@ -677,7 +743,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 0, 3000 })) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -701,15 +767,15 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1950, 2000 })) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1994, 1994 })); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release1.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release1.getId()); releases = Release::findIds(session, Release::FindParameters{}.setDateRange(YearRange{ 1993, 1993 })); - ASSERT_EQ(releases.results.size(), 0); + ASSERT_EQ(releases.size(), 0); } } @@ -729,19 +795,19 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}) }; - EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.size(), 1); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setWrittenAfter(dateTime.addSecs(-1))) }; - EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.size(), 1); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setWrittenAfter(dateTime.addSecs(+1))) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } } @@ -760,12 +826,12 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0); } @@ -782,39 +848,39 @@ namespace lms::db::tests EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })); - EXPECT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + EXPECT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId())); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Writer, TrackArtistLinkType::Artist })); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())); - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 1); EXPECT_EQ(release->getTrackArtists().size(), 1); EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Artist).size(), 1); EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer })); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer })); - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -850,7 +916,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 0); EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0); EXPECT_EQ(release->getArtistLinks().size(), 0); @@ -907,8 +973,8 @@ namespace lms::db::tests EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist1.getId())), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0); @@ -999,8 +1065,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto labels{ Label::findOrphanIds(session) }; - ASSERT_EQ(labels.results.size(), 1); - EXPECT_EQ(labels.results.front(), label.getId()); + ASSERT_EQ(labels.size(), 1); + EXPECT_EQ(labels.front(), label.getId()); } ScopedRelease release{ session, "MyRelease" }; @@ -1013,7 +1079,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto labels{ Label::findOrphanIds(session) }; - EXPECT_EQ(labels.results.size(), 0); + EXPECT_EQ(labels.size(), 0); } { @@ -1024,8 +1090,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto labels{ Label::findOrphanIds(session) }; - ASSERT_EQ(labels.results.size(), 1); - EXPECT_EQ(labels.results.front(), label.getId()); + ASSERT_EQ(labels.size(), 1); + EXPECT_EQ(labels.front(), label.getId()); } } @@ -1071,8 +1137,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto countries{ Country::findOrphanIds(session) }; - ASSERT_EQ(countries.results.size(), 1); - EXPECT_EQ(countries.results.front(), country.getId()); + ASSERT_EQ(countries.size(), 1); + EXPECT_EQ(countries.front(), country.getId()); } ScopedRelease release{ session, "MyRelease" }; @@ -1085,7 +1151,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto countries{ Country::findOrphanIds(session) }; - EXPECT_EQ(countries.results.size(), 0); + EXPECT_EQ(countries.size(), 0); } { @@ -1096,8 +1162,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto countries{ Country::findOrphanIds(session) }; - ASSERT_EQ(countries.results.size(), 1); - EXPECT_EQ(countries.results.front(), country.getId()); + ASSERT_EQ(countries.size(), 1); + EXPECT_EQ(countries.front(), country.getId()); } } @@ -1126,8 +1192,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto releaseTypes{ ReleaseType::findOrphanIds(session) }; - ASSERT_EQ(releaseTypes.results.size(), 1); - EXPECT_EQ(releaseTypes.results.front(), releaseType.getId()); + ASSERT_EQ(releaseTypes.size(), 1); + EXPECT_EQ(releaseTypes.front(), releaseType.getId()); } ScopedRelease release{ session, "MyRelease" }; @@ -1140,7 +1206,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto releaseTypes{ ReleaseType::findOrphanIds(session) }; - EXPECT_EQ(releaseTypes.results.size(), 0); + EXPECT_EQ(releaseTypes.size(), 0); } { @@ -1151,8 +1217,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto releaseTypes{ ReleaseType::findOrphanIds(session) }; - ASSERT_EQ(releaseTypes.results.size(), 1); - EXPECT_EQ(releaseTypes.results.front(), releaseType.getId()); + ASSERT_EQ(releaseTypes.size(), 1); + EXPECT_EQ(releaseTypes.front(), releaseType.getId()); } } @@ -1191,7 +1257,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::find(session, Release::FindParameters{}.setReleaseType("Foo")).results }; + auto releases{ Release::find(session, Release::FindParameters{}.setReleaseType("Foo")) }; EXPECT_EQ(releases.size(), 0); } @@ -1205,10 +1271,10 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::find(session, Release::FindParameters{}.setReleaseType("Foo")).results }; + auto releases{ Release::find(session, Release::FindParameters{}.setReleaseType("Foo")) }; EXPECT_EQ(releases.size(), 0); - releases = Release::find(session, Release::FindParameters{}.setReleaseType("album")).results; + releases = Release::find(session, Release::FindParameters{}.setReleaseType("album")); ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release.getId()); } @@ -1244,51 +1310,51 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::Name)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results.front(), release1.getId()); - EXPECT_EQ(releases.results.back(), release2.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases.front(), release1.getId()); + EXPECT_EQ(releases.back(), release2.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::Random)) }; - ASSERT_EQ(releases.results.size(), 2); + ASSERT_EQ(releases.size(), 2); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::DateAsc)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results.front(), release2.getId()); - EXPECT_EQ(releases.results.back(), release1.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases.front(), release2.getId()); + EXPECT_EQ(releases.back(), release1.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::DateDesc)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results.front(), release1.getId()); - EXPECT_EQ(releases.results.back(), release2.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases.front(), release1.getId()); + EXPECT_EQ(releases.back(), release2.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::OriginalDate)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results.front(), release1.getId()); - EXPECT_EQ(releases.results.back(), release2.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases.front(), release1.getId()); + EXPECT_EQ(releases.back(), release2.getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::OriginalDateDesc)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results.front(), release2.getId()); - EXPECT_EQ(releases.results.back(), release1.getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases.front(), release2.getId()); + EXPECT_EQ(releases.back(), release1.getId()); } } @@ -1392,8 +1458,8 @@ namespace lms::db::tests params.setFilters(Filters{}.setCodec(core::media::Codec::MP3)); auto releases{ Release::findIds(session, params) }; - ASSERT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results.front(), release2.getId()); + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release2.getId()); } } @@ -1491,11 +1557,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::AddedDesc)) }; - ASSERT_EQ(releases.results.size(), 4); - EXPECT_EQ(releases.results[0], releaseA.getId()); - EXPECT_EQ(releases.results[1], releaseD.getId()); - EXPECT_EQ(releases.results[2], releaseB.getId()); - EXPECT_EQ(releases.results[3], releaseC.getId()); + ASSERT_EQ(releases.size(), 4); + EXPECT_EQ(releases[0], releaseA.getId()); + EXPECT_EQ(releases[1], releaseD.getId()); + EXPECT_EQ(releases[2], releaseB.getId()); + EXPECT_EQ(releases[3], releaseC.getId()); } } @@ -1531,11 +1597,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::LastWrittenDesc)) }; - ASSERT_EQ(releases.results.size(), 4); - EXPECT_EQ(releases.results[0], releaseA.getId()); - EXPECT_EQ(releases.results[1], releaseD.getId()); - EXPECT_EQ(releases.results[2], releaseB.getId()); - EXPECT_EQ(releases.results[3], releaseC.getId()); + ASSERT_EQ(releases.size(), 4); + EXPECT_EQ(releases[0], releaseA.getId()); + EXPECT_EQ(releases[1], releaseD.getId()); + EXPECT_EQ(releases[2], releaseB.getId()); + EXPECT_EQ(releases[3], releaseC.getId()); } } @@ -1610,7 +1676,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::find(session, Release::FindParameters{}.setReleaseGroupMBID(groupMBID)) }; - EXPECT_EQ(releases.results.size(), 0); + EXPECT_EQ(releases.size(), 0); } { @@ -1622,8 +1688,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::find(session, Release::FindParameters{}.setReleaseGroupMBID(groupMBID)) }; - EXPECT_EQ(releases.results.size(), 1); - EXPECT_EQ(releases.results[0]->getId(), release->getId()); + EXPECT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0]->getId(), release->getId()); } } @@ -1642,18 +1708,18 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto releases{ Release::find(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::Name)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0]->getId(), release1->getId()); - EXPECT_EQ(releases.results[1]->getId(), release2->getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release1->getId()); + EXPECT_EQ(releases[1]->getId(), release2->getId()); } { auto transaction{ session.createReadTransaction() }; const auto releases{ Release::find(session, Release::FindParameters{}.setSortMethod(ReleaseSortMethod::SortName)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0]->getId(), release2->getId()); - EXPECT_EQ(releases.results[1]->getId(), release1->getId()); + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release2->getId()); + EXPECT_EQ(releases[1]->getId(), release1->getId()); } } @@ -1726,4 +1792,5 @@ namespace lms::db::tests EXPECT_EQ(mediums[1]->getId(), medium2.getId()); } } + } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/StarredArtist.cpp b/src/libs/database/test/StarredArtist.cpp index f85af565..fcfee416 100644 --- a/src/libs/database/test/StarredArtist.cpp +++ b/src/libs/database/test/StarredArtist.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(StarredArtist::getCount(session), 0); auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - EXPECT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.size(), 1); } ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; @@ -55,13 +55,13 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; - EXPECT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.size(), 1); - artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(artists.results.size(), 1); + artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(artists.size(), 1); - artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(artists.results.size(), 0); + artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId())); + EXPECT_EQ(artists.size(), 0); } { @@ -97,12 +97,12 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(artists.results.size(), 1); + auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(artists.size(), 1); starredArtist.get().modify()->setSyncState(SyncState::PendingRemove); - artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(artists.results.size(), 0); + artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(artists.size(), 0); } } @@ -120,8 +120,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto artists{ Artist::find(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(artists.results.size(), 2); + auto artists{ Artist::find(session, Artist::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(artists.size(), 2); } { @@ -130,10 +130,10 @@ namespace lms::db::tests starredArtist1.get().modify()->setDateTime(dateTime); starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1)); - auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId()); - EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId()); + auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId()).setSortMethod(ArtistSortMethod::StarredDateDesc)) }; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], starredArtist1->getArtist()->getId()); + EXPECT_EQ(artists[1], starredArtist2->getArtist()->getId()); } { auto transaction{ session.createWriteTransaction() }; @@ -141,10 +141,34 @@ namespace lms::db::tests starredArtist1.get().modify()->setDateTime(dateTime); starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1)); - auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) }; - ASSERT_EQ(artists.results.size(), 2); - EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId()); - EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId()); + auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId()).setSortMethod(ArtistSortMethod::StarredDateDesc)) }; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0], starredArtist2->getArtist()->getId()); + EXPECT_EQ(artists[1], starredArtist1->getArtist()->getId()); + } + } + TEST_F(DatabaseFixture, StarredArtist_currentBackend) + { + ScopedArtist artist{ session, "MyArtist" }; + ScopedUser user{ session, "MyUser" }; + ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; + + { + auto transaction{ session.createReadTransaction() }; + auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId())) }; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists[0], artist->getId()); + } + + { + auto transaction{ session.createWriteTransaction() }; + user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz); + } + + { + auto transaction{ session.createReadTransaction() }; + auto artists{ Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(artists.size(), 0); } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/StarredRelease.cpp b/src/libs/database/test/StarredRelease.cpp index d8e66a35..ba06439d 100644 --- a/src/libs/database/test/StarredRelease.cpp +++ b/src/libs/database/test/StarredRelease.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(StarredRelease::getCount(session), 0); auto releases{ Release::find(session, Release::FindParameters{}) }; - EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.size(), 1); } ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; @@ -55,13 +55,13 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto releases{ Release::find(session, Release::FindParameters{}) }; - EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.size(), 1); - releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(releases.results.size(), 1); + releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(releases.size(), 1); - releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(releases.results.size(), 0); + releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId())); + EXPECT_EQ(releases.size(), 0); } { @@ -86,12 +86,12 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto releases{ Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(releases.results.size(), 1); + auto releases{ Release::find(session, Release::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(releases.size(), 1); starredRelease.get().modify()->setSyncState(SyncState::PendingRemove); - releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(releases.results.size(), 0); + releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(releases.size(), 0); } } @@ -109,8 +109,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(releases.results.size(), 2); + auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(releases.size(), 2); } { @@ -119,10 +119,10 @@ namespace lms::db::tests starredRelease1.get().modify()->setDateTime(dateTime); starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1)); - auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId()); - EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId()); + auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId()).setSortMethod(ReleaseSortMethod::StarredDateDesc)) }; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], starredRelease1->getRelease()->getId()); + EXPECT_EQ(releases[1], starredRelease2->getRelease()->getId()); } { auto transaction{ session.createWriteTransaction() }; @@ -130,10 +130,34 @@ namespace lms::db::tests starredRelease1.get().modify()->setDateTime(dateTime); starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1)); - auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) }; - ASSERT_EQ(releases.results.size(), 2); - EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId()); - EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId()); + auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId()).setSortMethod(ReleaseSortMethod::StarredDateDesc)) }; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0], starredRelease2->getRelease()->getId()); + EXPECT_EQ(releases[1], starredRelease1->getRelease()->getId()); + } + } + TEST_F(DatabaseFixture, StarredRelease_currentBackend) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedUser user{ session, "MyUser" }; + ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; + + { + auto transaction{ session.createReadTransaction() }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId())) }; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases[0], release->getId()); + } + + { + auto transaction{ session.createWriteTransaction() }; + user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz); + } + + { + auto transaction{ session.createReadTransaction() }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(releases.size(), 0); } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/StarredTrack.cpp b/src/libs/database/test/StarredTrack.cpp index 0c0b4632..20b8d239 100644 --- a/src/libs/database/test/StarredTrack.cpp +++ b/src/libs/database/test/StarredTrack.cpp @@ -39,7 +39,7 @@ namespace lms::db::tests EXPECT_EQ(StarredTrack::getCount(session), 0); auto tracks{ Track::findIds(session, Track::FindParameters{}) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); } ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; @@ -55,13 +55,13 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); - tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(tracks.results.size(), 1); + tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(tracks.size(), 1); - tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(tracks.results.size(), 0); + tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user2.getId())); + EXPECT_EQ(tracks.size(), 0); } { @@ -86,12 +86,12 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(tracks.results.size(), 1); + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(tracks.size(), 1); starredTrack.get().modify()->setSyncState(SyncState::PendingRemove); - tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); - EXPECT_EQ(tracks.results.size(), 0); + tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())); + EXPECT_EQ(tracks.size(), 0); } } @@ -109,8 +109,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; - EXPECT_EQ(tracks.results.size(), 2); + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(tracks.size(), 2); } { @@ -119,10 +119,10 @@ namespace lms::db::tests starredTrack1.get().modify()->setDateTime(dateTime); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); - auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); - EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId()).setSortMethod(TrackSortMethod::StarredDateDesc)) }; + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], starredTrack1->getTrack()->getId()); + EXPECT_EQ(tracks[1], starredTrack2->getTrack()->getId()); } { auto transaction{ session.createWriteTransaction() }; @@ -130,10 +130,34 @@ namespace lms::db::tests starredTrack1.get().modify()->setDateTime(dateTime); starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); - auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); - EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId()).setSortMethod(TrackSortMethod::StarredDateDesc)) }; + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], starredTrack2->getTrack()->getId()); + EXPECT_EQ(tracks[1], starredTrack1->getTrack()->getId()); + } + } + TEST_F(DatabaseFixture, StarredTrack_currentBackend) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; + + { + auto transaction{ session.createReadTransaction() }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())) }; + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0], track->getId()); + } + + { + auto transaction{ session.createWriteTransaction() }; + user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz); + } + + { + auto transaction{ session.createReadTransaction() }; + auto tracks{ Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId())) }; + EXPECT_EQ(tracks.size(), 0); } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index f68c7ebf..feaf7d94 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -37,8 +37,8 @@ namespace lms::db::tests { { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0); - EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0); + EXPECT_EQ(Track::find(session, Track::FindParameters{}).size(), 0); + EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).size(), 0); EXPECT_EQ(Track::getCount(session), 0); EXPECT_FALSE(Track::exists(session, 0)); @@ -54,7 +54,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1); + EXPECT_EQ(Track::find(session, Track::FindParameters{}).size(), 1); EXPECT_EQ(Track::getCount(session), 1); EXPECT_TRUE(Track::exists(session, track.getId())); auto myTrack{ Track::find(session, track.getId()) }; @@ -260,8 +260,8 @@ namespace lms::db::tests params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC)); const auto tracks{ Track::find(session, params) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results[0]->getId(), track2.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); } } @@ -291,13 +291,13 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); } { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -320,7 +320,7 @@ namespace lms::db::tests EXPECT_FALSE(Track::exists(session, TrackId{ 42 })); EXPECT_EQ(Track::find(session, TrackId{ 42 }), Track::pointer{}); EXPECT_FALSE(Track::find(session, TrackId{ 42 })); - EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0); + EXPECT_EQ(Track::find(session, Track::FindParameters{}).size(), 0); { auto track{ Track::find(session, TrackId{ 42 }) }; EXPECT_TRUE(!track); @@ -366,24 +366,24 @@ namespace lms::db::tests { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setKeywords({ "Track" })) }; - EXPECT_EQ(tracks.results.size(), 6); + EXPECT_EQ(tracks.size(), 6); } { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setKeywords({ "MyTrack" })) }; - EXPECT_EQ(tracks.results.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); + EXPECT_EQ(tracks.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const TrackId trackId) { return trackId == track6.getId(); })); } { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setKeywords({ "MyTrack%" })) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track2.getId()); - EXPECT_EQ(tracks.results[1], track3.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track2.getId()); + EXPECT_EQ(tracks[1], track3.getId()); } { const auto tracks{ Track::findIds(session, Track::FindParameters{}.setKeywords({ "%MyTrack" })) }; - ASSERT_EQ(tracks.results.size(), 2); - EXPECT_EQ(tracks.results[0], track4.getId()); - EXPECT_EQ(tracks.results[1], track5.getId()); + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0], track4.getId()); + EXPECT_EQ(tracks[1], track5.getId()); } } } @@ -434,19 +434,19 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); } { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setWrittenAfter(dateTime.addSecs(-1))) }; - EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.size(), 1); } { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setWrittenAfter(dateTime.addSecs(+1))) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -525,11 +525,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setSortMethod(TrackSortMethod::AddedDesc)) }; - ASSERT_EQ(tracks.results.size(), 4); - EXPECT_EQ(tracks.results[0], track4.getId()); - EXPECT_EQ(tracks.results[1], track1.getId()); - EXPECT_EQ(tracks.results[2], track2.getId()); - EXPECT_EQ(tracks.results[3], track3.getId()); + ASSERT_EQ(tracks.size(), 4); + EXPECT_EQ(tracks[0], track4.getId()); + EXPECT_EQ(tracks[1], track1.getId()); + EXPECT_EQ(tracks[2], track2.getId()); + EXPECT_EQ(tracks[3], track3.getId()); } } @@ -550,11 +550,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setSortMethod(TrackSortMethod::LastWrittenDesc)) }; - ASSERT_EQ(tracks.results.size(), 4); - EXPECT_EQ(tracks.results[0], track4.getId()); - EXPECT_EQ(tracks.results[1], track1.getId()); - EXPECT_EQ(tracks.results[2], track2.getId()); - EXPECT_EQ(tracks.results[3], track3.getId()); + ASSERT_EQ(tracks.size(), 4); + EXPECT_EQ(tracks[0], track4.getId()); + EXPECT_EQ(tracks[1], track1.getId()); + EXPECT_EQ(tracks[2], track2.getId()); + EXPECT_EQ(tracks[3], track3.getId()); } } @@ -619,8 +619,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; const auto results{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre.getId()).setMood(mood.getId()).setGrouping(grouping.getId()).setLanguage(language.getId()))) }; - ASSERT_EQ(results.results.size(), 1); - EXPECT_EQ(results.results.front(), track1.getId()); + ASSERT_EQ(results.size(), 1); + EXPECT_EQ(results.front(), track1.getId()); } } } // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/database/test/TrackBookmark.cpp b/src/libs/database/test/TrackBookmark.cpp index 5e4d8208..876135a5 100644 --- a/src/libs/database/test/TrackBookmark.cpp +++ b/src/libs/database/test/TrackBookmark.cpp @@ -50,8 +50,8 @@ namespace lms::db::tests EXPECT_EQ(TrackBookmark::getCount(session), 1); const auto bookmarks{ TrackBookmark::find(session, user.getId()) }; - ASSERT_EQ(bookmarks.results.size(), 1); - EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); + ASSERT_EQ(bookmarks.size(), 1); + EXPECT_EQ(bookmarks.front(), bookmark.getId()); } { auto transaction{ session.createReadTransaction() }; diff --git a/src/libs/database/test/TrackEmbeddedImage.cpp b/src/libs/database/test/TrackEmbeddedImage.cpp index 61bcda1a..02ada518 100644 --- a/src/libs/database/test/TrackEmbeddedImage.cpp +++ b/src/libs/database/test/TrackEmbeddedImage.cpp @@ -364,8 +364,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results[0], image.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans[0], image.getId()); } { @@ -376,7 +376,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; - ASSERT_EQ(orphans.results.size(), 0); + ASSERT_EQ(orphans.size(), 0); } } @@ -384,8 +384,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) }; - ASSERT_EQ(orphans.results.size(), 1); - EXPECT_EQ(orphans.results[0], image.getId()); + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans[0], image.getId()); } } diff --git a/src/libs/database/test/TrackList.cpp b/src/libs/database/test/TrackList.cpp index 33320ebb..b0f5db48 100644 --- a/src/libs/database/test/TrackList.cpp +++ b/src/libs/database/test/TrackList.cpp @@ -53,10 +53,10 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setTrackList(trackList1.getId())) }; - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } { @@ -69,11 +69,11 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setTrackList(trackList1.getId())) }; - ASSERT_EQ(tracks.results.size(), 1); - EXPECT_EQ(tracks.results.front(), track.getId()); + ASSERT_EQ(tracks.size(), 1); + EXPECT_EQ(tracks.front(), track.getId()); tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId())); - EXPECT_EQ(tracks.results.size(), 0); + EXPECT_EQ(tracks.size(), 0); } } @@ -87,9 +87,9 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; 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()); + ASSERT_EQ(trackLists.size(), 2); + EXPECT_EQ(trackLists[0], trackList1.getId()); + EXPECT_EQ(trackLists[1], trackList2.getId()); } { @@ -103,9 +103,9 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; 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()); + ASSERT_EQ(trackLists.size(), 2); + EXPECT_EQ(trackLists[0], trackList2.getId()); + EXPECT_EQ(trackLists[1], trackList1.getId()); } { @@ -119,9 +119,9 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; 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()); + ASSERT_EQ(trackLists.size(), 2); + EXPECT_EQ(trackLists[0], trackList1.getId()); + EXPECT_EQ(trackLists[1], trackList2.getId()); } } @@ -245,7 +245,7 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))) }; - EXPECT_EQ(trackLists.results.size(), 0); + EXPECT_EQ(trackLists.size(), 0); } { @@ -259,8 +259,8 @@ namespace lms::db::tests auto transaction{ session.createReadTransaction() }; auto trackLists{ TrackList::find(session, TrackList::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))) }; - ASSERT_EQ(trackLists.results.size(), 1); - EXPECT_EQ(trackLists.results.front(), trackList1.getId()); + ASSERT_EQ(trackLists.size(), 1); + EXPECT_EQ(trackLists.front(), trackList1.getId()); } } @@ -278,8 +278,8 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto entries{ trackList.get()->getEntries() }; - ASSERT_EQ(entries.results.size(), 1); - EXPECT_EQ(entries.results.front()->getTrack()->getId(), track1.getId()); + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); } { @@ -290,16 +290,16 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; auto entries{ trackList.get()->getEntries() }; - ASSERT_EQ(entries.results.size(), 2); - EXPECT_EQ(entries.results[0]->getTrack()->getId(), track1.getId()); - EXPECT_EQ(entries.results[1]->getTrack()->getId(), track2.getId()); + ASSERT_EQ(entries.size(), 2); + EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); + EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); } { auto transaction{ session.createReadTransaction() }; auto entries{ trackList.get()->getEntries(Range{ 1, 1 }) }; - ASSERT_EQ(entries.results.size(), 1); - EXPECT_EQ(entries.results[0]->getTrack()->getId(), track2.getId()); + ASSERT_EQ(entries.size(), 1); + EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); } } diff --git a/src/libs/database/test/Work.cpp b/src/libs/database/test/Work.cpp new file mode 100644 index 00000000..846a949a --- /dev/null +++ b/src/libs/database/test/Work.cpp @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2025 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 "Common.hpp" + +#include "database/objects/Work.hpp" + +namespace lms::db::tests +{ + using ScopedWork = ScopedEntity; + + TEST_F(DatabaseFixture, Work_create) + { + ScopedWork work{ session, "Symphony No. 5", std::optional{} }; + + { + auto transaction{ session.createReadTransaction() }; + + const Work::pointer found{ Work::find(session, work.getId()) }; + ASSERT_TRUE(found); + EXPECT_EQ(found->getName(), "Symphony No. 5"); + EXPECT_FALSE(found->getMBID()); + } + } + + TEST_F(DatabaseFixture, Work_createWithMBID) + { + const auto mbid{ core::UUID::fromString("8f3471b3-7e93-4de1-a9c7-3b843c21b84e") }; + ASSERT_TRUE(mbid); + + ScopedWork work{ session, "Symphony No. 9", mbid }; + + { + auto transaction{ session.createReadTransaction() }; + + const Work::pointer found{ Work::find(session, work.getId()) }; + ASSERT_TRUE(found); + EXPECT_EQ(found->getName(), "Symphony No. 9"); + ASSERT_TRUE(found->getMBID()); + EXPECT_EQ(found->getMBID(), mbid); + } + } + + TEST_F(DatabaseFixture, Work_findByMBID) + { + const auto mbid{ core::UUID::fromString("8f3471b3-7e93-4de1-a9c7-3b843c21b84e") }; + ASSERT_TRUE(mbid); + + ScopedWork workWithMbid{ session, "Piano Sonata", mbid }; + + { + auto transaction{ session.createReadTransaction() }; + + const Work::pointer byMbid{ Work::find(session, *mbid) }; + ASSERT_TRUE(byMbid); + EXPECT_EQ(byMbid->getId(), workWithMbid.getId()); + } + } + + TEST_F(DatabaseFixture, Work_findByNameScopedToRelease) + { + // Work titles are often generic (e.g. "Piano Sonata"): two unrelated releases can each have + // their own work with the exact same name, and lookup must not merge them + ScopedRelease release1{ session, "Release1" }; + ScopedRelease release2{ session, "Release2" }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedWork work1{ session, "Piano Sonata", std::optional{} }; + ScopedWork work2{ session, "Piano Sonata", std::optional{} }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setRelease(release1.get()); + track1.get().modify()->setWorks(std::array{ work1.get() }); + track2.get().modify()->setRelease(release2.get()); + track2.get().modify()->setWorks(std::array{ work2.get() }); + } + + { + auto transaction{ session.createReadTransaction() }; + + // Same name, but each release resolves to its own work + const Work::pointer foundInRelease1{ Work::find(session, release1.getId(), "Piano Sonata") }; + ASSERT_TRUE(foundInRelease1); + EXPECT_EQ(foundInRelease1->getId(), work1.getId()); + + const Work::pointer foundInRelease2{ Work::find(session, release2.getId(), "Piano Sonata") }; + ASSERT_TRUE(foundInRelease2); + EXPECT_EQ(foundInRelease2->getId(), work2.getId()); + + // Unknown name in a known release returns null + EXPECT_FALSE(Work::find(session, release1.getId(), "Unknown Work")); + } + } + + TEST_F(DatabaseFixture, Work_orphan) + { + ScopedWork work{ session, "Unlinked Work", std::optional{} }; + + { + auto transaction{ session.createReadTransaction() }; + const auto orphans{ Work::findOrphanIds(session) }; + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), work.getId()); + } + } + + TEST_F(DatabaseFixture, Work_singleTrack) + { + ScopedTrack track{ session }; + ScopedWork work1{ session, "Requiem", std::optional{} }; + ScopedWork work2{ session, "Missa Solemnis", std::optional{} }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Work::findOrphanIds(session).size(), 2); + EXPECT_EQ(track->getWorks().size(), 0); + } + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setWorks(std::array{ work1.get() }); + } + + { + auto transaction{ session.createReadTransaction() }; + + const auto orphans{ Work::findOrphanIds(session) }; + ASSERT_EQ(orphans.size(), 1); + EXPECT_EQ(orphans.front(), work2.getId()); + + const auto trackWorks{ track->getWorks() }; + ASSERT_EQ(trackWorks.size(), 1); + EXPECT_EQ(trackWorks.front()->getId(), work1.getId()); + } + } + + TEST_F(DatabaseFixture, Work_multipleWorksOnTrack) + { + ScopedTrack track{ session }; + ScopedWork work1{ session, "Symphony No. 5", std::optional{} }; + ScopedWork work2{ session, "Symphony No. 6", std::optional{} }; + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setWorks(std::array{ work1.get(), work2.get() }); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Work::findOrphanIds(session).size(), 0); + + const auto trackWorks{ track->getWorks() }; + EXPECT_EQ(trackWorks.size(), 2); + } + } + + TEST_F(DatabaseFixture, Work_multipleTracksOnWork) + { + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedTrack track3{ session }; + ScopedWork work{ session, "The Four Seasons", std::optional{} }; + + { + auto transaction{ session.createWriteTransaction() }; + track1.get().modify()->setWorks(std::array{ work.get() }); + track2.get().modify()->setWorks(std::array{ work.get() }); + track3.get().modify()->setWorks(std::array{ work.get() }); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Work::findOrphanIds(session).size(), 0); + + EXPECT_EQ(track1->getWorks().size(), 1); + EXPECT_EQ(track2->getWorks().size(), 1); + EXPECT_EQ(track3->getWorks().size(), 1); + } + } + + TEST_F(DatabaseFixture, Work_nameTruncation) + { + const std::string longName(Work::maxNameLength + 100, 'x'); + const std::string expectedName(Work::maxNameLength, 'x'); + + { + auto transaction{ session.createWriteTransaction() }; + Work::pointer work{ session.create(longName, std::optional{}) }; + ASSERT_TRUE(work); + EXPECT_EQ(work->getName().size(), Work::maxNameLength); + EXPECT_EQ(work->getName(), expectedName); + work.remove(); + } + } + +} // namespace lms::db::tests diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index 8a8e7875..5b0c6d8f 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -169,7 +169,7 @@ namespace lms::artwork } const auto entries{ trackList->getEntries(db::Range{ 0, 10 }) }; - for (const auto& entry : entries.results) + for (const auto& entry : entries) { const auto track{ entry->getTrack() }; if (track->getPreferredMediaArtworkId().isValid()) diff --git a/src/libs/services/auth/impl/AuthTokenService.cpp b/src/libs/services/auth/impl/AuthTokenService.cpp index 5fef93db..07ffe940 100644 --- a/src/libs/services/auth/impl/AuthTokenService.cpp +++ b/src/libs/services/auth/impl/AuthTokenService.cpp @@ -87,8 +87,19 @@ namespace lms::auth } } + bool AuthTokenService::isClientThrottled(const boost::asio::ip::address& clientAddress) const + { + std::shared_lock lock{ _mutex }; + + return _loginThrottler.isClientThrottled(clientAddress); + } + std::optional AuthTokenService::processAuthToken(core::LiteralString domain, std::string_view token) { + // An empty token must never match: some callers use it as a "no candidate" sentinel value, + if (token.empty()) + return std::nullopt; + db::Session& session{ getDbSession() }; auto transaction{ session.createWriteTransaction() }; diff --git a/src/libs/services/auth/impl/AuthTokenService.hpp b/src/libs/services/auth/impl/AuthTokenService.hpp index c0206774..ebe7a358 100644 --- a/src/libs/services/auth/impl/AuthTokenService.hpp +++ b/src/libs/services/auth/impl/AuthTokenService.hpp @@ -47,6 +47,7 @@ namespace lms::auth private: void registerDomain(core::LiteralString domain, const DomainParameters& params) override; + bool isClientThrottled(const boost::asio::ip::address& clientAddress) const override; AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override; void visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function visitor) override; void createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token) override; @@ -55,7 +56,7 @@ namespace lms::auth std::optional processAuthToken(core::LiteralString domain, std::string_view tokenValue); const DomainParameters& getDomainParameters(core::LiteralString domain) const; - std::shared_mutex _mutex; + mutable std::shared_mutex _mutex; std::map _domainParameters; LoginThrottler _loginThrottler; }; diff --git a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp index 300d4b32..3a6eb8e3 100644 --- a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp +++ b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp @@ -72,6 +72,8 @@ namespace lms::auth virtual void registerDomain(core::LiteralString domain, const DomainParameters& params) = 0; + virtual bool isClientThrottled(const boost::asio::ip::address& clientAddress) const = 0; + // Processing an auth token will make its useCount increase by 1. Token is then automatically deleted if its maxUsecount is reached virtual AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp index 1645626f..662c2116 100644 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -92,13 +92,9 @@ namespace lms::feedback FeedbackService::ArtistContainer FeedbackService::findStarredArtists(const ArtistFindParameters& params) { - auto backend{ getUserFeedbackBackend(params.user) }; - if (!backend) - return {}; - Artist::FindParameters searchParams; searchParams.setFilters(params.filters); - searchParams.setStarringUser(params.user, *backend); + searchParams.setStarringUser(params.user); searchParams.setKeywords(params.keywords); searchParams.setTrackArtistLinkType(params.trackArtistLinkType); searchParams.setSortMethod(params.sortMethod); @@ -142,12 +138,8 @@ namespace lms::feedback FeedbackService::ReleaseContainer FeedbackService::findStarredReleases(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(params.user) }; - if (!backend) - return {}; - Release::FindParameters searchParams; - searchParams.setStarringUser(params.user, *backend); + searchParams.setStarringUser(params.user); searchParams.setFilters(params.filters); searchParams.setKeywords(params.keywords); searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc); @@ -191,12 +183,8 @@ namespace lms::feedback FeedbackService::TrackContainer FeedbackService::findStarredTracks(const FindParameters& params) { - auto backend{ getUserFeedbackBackend(params.user) }; - if (!backend) - return {}; - Track::FindParameters searchParams; - searchParams.setStarringUser(params.user, *backend); + searchParams.setStarringUser(params.user); searchParams.setFilters(params.filters); searchParams.setKeywords(params.keywords); searchParams.setSortMethod(TrackSortMethod::StarredDateDesc); diff --git a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp index 2818ae13..b70839de 100644 --- a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp @@ -196,7 +196,7 @@ namespace lms::feedback::listenBrainz using namespace db; auto processPendingFeedbacks{ [this](SyncState scrobblingState, FeedbackType feedbackType) { - RangeResults pendingFeedbacks; + std::vector pendingFeedbacks; { db::Session& session{ _db.getTLSSession() }; @@ -210,9 +210,9 @@ namespace lms::feedback::listenBrainz pendingFeedbacks = StarredTrack::find(session, params); } - LOG(DEBUG, "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks"); + LOG(DEBUG, "Queing " << pendingFeedbacks.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks"); - for (const StarredTrackId starredTrackId : pendingFeedbacks.results) + for (const StarredTrackId starredTrackId : pendingFeedbacks) enqueFeedback(feedbackType, starredTrackId); } }; @@ -270,14 +270,14 @@ namespace lms::feedback::listenBrainz enquePendingFeedbacks(); - db::RangeResults userIds; + std::vector userIds; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; userIds = db::User::find(_db.getTLSSession(), db::User::FindParameters{}.setFeedbackBackend(db::FeedbackBackend::ListenBrainz)); } - for (const db::UserId userId : userIds.results) + for (const db::UserId userId : userIds) startSync(getUserContext(userId)); if (!isSyncing()) diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index 519bbcad..7c9f735c 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -45,9 +45,9 @@ namespace lms::feedback public: virtual ~IFeedbackService() = default; - using ArtistContainer = db::RangeResults; - using ReleaseContainer = db::RangeResults; - using TrackContainer = db::RangeResults; + using ArtistContainer = std::vector; + using ReleaseContainer = std::vector; + using TrackContainer = std::vector; struct FindParameters { diff --git a/src/libs/services/podcast/CMakeLists.txt b/src/libs/services/podcast/CMakeLists.txt index 1921939b..18be7584 100644 --- a/src/libs/services/podcast/CMakeLists.txt +++ b/src/libs/services/podcast/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(lmspodcast STATIC impl/Executor.cpp impl/PodcastParsing.cpp impl/PodcastService.cpp + impl/UrlValidation.cpp ) target_include_directories(lmspodcast INTERFACE diff --git a/src/libs/services/podcast/impl/UrlValidation.cpp b/src/libs/services/podcast/impl/UrlValidation.cpp new file mode 100644 index 00000000..2ef78cf3 --- /dev/null +++ b/src/libs/services/podcast/impl/UrlValidation.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 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 "UrlValidation.hpp" + +#include "core/http/UrlValidation.hpp" + +namespace lms::podcast +{ + bool isAllowedPodcastUrl(std::string_view url) + { + return core::http::isValidUrl(url); + } +} // namespace lms::podcast diff --git a/src/libs/services/podcast/impl/UrlValidation.hpp b/src/libs/services/podcast/impl/UrlValidation.hpp new file mode 100644 index 00000000..40f45122 --- /dev/null +++ b/src/libs/services/podcast/impl/UrlValidation.hpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 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 + +namespace lms::podcast +{ + bool isAllowedPodcastUrl(std::string_view url); +} // namespace lms::podcast diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp b/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp index 1cc6bd6a..6bb06531 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodeArtworksStep.cpp @@ -25,11 +25,14 @@ #include "core/ILogger.hpp" #include "core/http/IClient.hpp" + #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Image.hpp" #include "database/objects/PodcastEpisode.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" #include "Executor.hpp" #include "Utils.hpp" @@ -122,6 +125,19 @@ namespace lms::podcast }; params.onSuccessFunc = [=, this](const Wt::Http::Message& msg) { getExecutor().post([=, this] { + const std::string body{ msg.body() }; // API enforces a copy here :( + const auto bodySpan{ std::as_bytes(std::span{ body.data(), body.size() }) }; + try + { + image::probeImage(bodySpan); + } + catch (const image::Exception& e) + { + LMS_LOG(PODCAST, WARNING, "Discarding non-image response for episode '" << episode->getTitle() << "' from '" << url << "': " << e.what()); + processNext(); + return; + } + std::ofstream file{ finalFilePath, std::ios::binary | std::ios::trunc }; if (!file) { @@ -131,8 +147,7 @@ namespace lms::podcast return; } - const std::string body{ msg.body() }; // API enforces a copy here - file.write(body.data(), body.size()); + file.write(body.data(), static_cast(body.size())); if (!file) { std::error_code ec{ errno, std::generic_category() }; diff --git a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp index a3704984..0953f069 100644 --- a/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadEpisodesStep.cpp @@ -212,11 +212,13 @@ namespace lms::podcast else { LMS_LOG(PODCAST, WARNING, "Failed to get audio properties from downloaded episode from '" << url << "'"); + utils::removeFile(tmpFilePath); } } catch (const audio::Exception& e) { LMS_LOG(PODCAST, WARNING, "Failed to parse downloaded episode from '" << url << "': " << e.what()); + utils::removeFile(tmpFilePath); } processNext(); diff --git a/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp b/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp index 2db0e696..352bccdb 100644 --- a/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp +++ b/src/libs/services/podcast/impl/steps/DownloadPodcastArtworksStep.cpp @@ -29,6 +29,8 @@ #include "database/Session.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Podcast.hpp" +#include "image/Exception.hpp" +#include "image/Image.hpp" #include "Executor.hpp" #include "Utils.hpp" @@ -121,9 +123,20 @@ namespace lms::podcast }; params.onSuccessFunc = [=, this](const Wt::Http::Message& msg) { getExecutor().post([=, this] { - const std::string body{ msg.body() }; // API enforces a copy here + const std::string body{ msg.body() }; // API enforces a copy here :( + const auto bodySpan{ std::as_bytes(std::span{ body.data(), body.size() }) }; + try + { + image::probeImage(bodySpan); + } + catch (const image::Exception& e) + { + LMS_LOG(PODCAST, WARNING, "Discarding non-image response for podcast artwork from '" << url << "': " << e.what()); + processNext(); + return; + } - std::ofstream file{ finalFilePath, std::ios::binary | std::ios::app }; + std::ofstream file{ finalFilePath, std::ios::binary | std::ios::trunc }; if (!file) { std::error_code ec{ errno, std::generic_category() }; @@ -132,7 +145,7 @@ namespace lms::podcast return; } - file.write(body.data(), body.size()); + file.write(body.data(), static_cast(body.size())); if (!file) { std::error_code ec{ errno, std::generic_category() }; diff --git a/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp b/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp index d43561ce..0ac7233f 100644 --- a/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp +++ b/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp @@ -31,6 +31,7 @@ #include "Executor.hpp" #include "PodcastParsing.hpp" #include "PodcastTypes.hpp" +#include "UrlValidation.hpp" namespace lms::podcast { @@ -49,6 +50,40 @@ namespace lms::podcast image.remove(); } + void addEpisode(db::Session& session, const db::Podcast::pointer& dbPodcast, const PodcastEpisode& episode) + { + LMS_LOG(PODCAST, DEBUG, "Adding episode '" << episode.title << "' to podcast '" << dbPodcast->getTitle() << "'"); + + auto dbEpisode{ session.create(dbPodcast) }; + + dbEpisode.modify()->setAuthor(episode.author); + dbEpisode.modify()->setCategory(episode.category); + dbEpisode.modify()->setDescription(episode.description); + + if (isAllowedPodcastUrl(episode.enclosureUrl.url)) + { + dbEpisode.modify()->setEnclosureUrl(episode.enclosureUrl.url); + dbEpisode.modify()->setEnclosureContentType(episode.enclosureUrl.type); + dbEpisode.modify()->setEnclosureLength(episode.enclosureUrl.length); + } + else + { + LMS_LOG(PODCAST, WARNING, "Episode '" << episode.title << "' : ignoring enclosure URL '" << episode.enclosureUrl.url << "' (bad URL)"); + } + + dbEpisode.modify()->setExplicit(episode.explicitContent ? *episode.explicitContent : false); + dbEpisode.modify()->setLink(episode.link); + dbEpisode.modify()->setPubDate(episode.pubDate); + dbEpisode.modify()->setTitle(episode.title); + + if (isAllowedPodcastUrl(episode.imageUrl)) + dbEpisode.modify()->setImageUrl(episode.imageUrl); + else if (!episode.imageUrl.empty()) + LMS_LOG(PODCAST, WARNING, "Episode '" << episode.title << "' : ignoring image URL '" << episode.imageUrl << "' (bad URL)"); + + dbEpisode.modify()->setDuration(episode.duration); + } + void updatePodcast(db::Session& session, db::PodcastId podcastId, const Podcast& podcast) { auto transaction{ session.createWriteTransaction() }; @@ -62,8 +97,15 @@ namespace lms::podcast // force update the podcast data if (!podcast.newUrl.empty() && podcast.newUrl != dbPodcast->getUrl()) { - LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : URL changed from '" << dbPodcast->getUrl() << "' to '" << podcast.newUrl << "'"); - dbPodcast.modify()->setUrl(podcast.newUrl); + if (isAllowedPodcastUrl(podcast.newUrl)) + { + LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : URL changed from '" << dbPodcast->getUrl() << "' to '" << podcast.newUrl << "'"); + dbPodcast.modify()->setUrl(podcast.newUrl); + } + else + { + LMS_LOG(PODCAST, WARNING, "Podcast '" << podcast.title << "' : ignoring new podcast URL '" << podcast.newUrl << "' (bad URL)"); + } } dbPodcast.modify()->setAuthor(podcast.author); dbPodcast.modify()->setCategory(podcast.category); @@ -80,14 +122,21 @@ namespace lms::podcast dbPodcast.modify()->setTitle(podcast.title); if (std::string previousUrl{ dbPodcast->getImageUrl() }; !previousUrl.empty() && previousUrl != podcast.imageUrl) { - LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : image url changed from '" << previousUrl << "' to '" << podcast.imageUrl << "'"); - if (db::Artwork::pointer currentArtwork{ dbPodcast->getArtwork() }) + if (isAllowedPodcastUrl(podcast.imageUrl)) { - removeArtwork(currentArtwork); - dbPodcast.modify()->setArtwork({}); - } + LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : image url changed from '" << previousUrl << "' to '" << podcast.imageUrl << "'"); + if (db::Artwork::pointer currentArtwork{ dbPodcast->getArtwork() }) + { + removeArtwork(currentArtwork); + dbPodcast.modify()->setArtwork({}); + } - dbPodcast.modify()->setImageUrl(podcast.imageUrl); + dbPodcast.modify()->setImageUrl(podcast.imageUrl); + } + else + { + LMS_LOG(PODCAST, WARNING, "Podcast '" << podcast.title << "' : ignoring image URL '" << podcast.imageUrl << "' (disallowed scheme)"); + } } // Only create episodes if they are new, do not modify/update existing entries for now @@ -102,22 +151,7 @@ namespace lms::podcast if (previousNewestEpisodeDateTime.isValid() && episode.pubDate <= previousNewestEpisodeDateTime) continue; // consider already in db - LMS_LOG(PODCAST, DEBUG, "Adding episode '" << episode.title << "' to podcast '" << podcast.title << "'"); - - auto dbEpisode{ session.create(dbPodcast) }; - - dbEpisode.modify()->setAuthor(episode.author); - dbEpisode.modify()->setCategory(episode.category); - dbEpisode.modify()->setDescription(episode.description); - dbEpisode.modify()->setEnclosureUrl(episode.enclosureUrl.url); - dbEpisode.modify()->setEnclosureContentType(episode.enclosureUrl.type); - dbEpisode.modify()->setEnclosureLength(episode.enclosureUrl.length); - dbEpisode.modify()->setExplicit(episode.explicitContent ? *episode.explicitContent : false); - dbEpisode.modify()->setLink(episode.link); - dbEpisode.modify()->setPubDate(episode.pubDate); - dbEpisode.modify()->setTitle(episode.title); - dbEpisode.modify()->setImageUrl(episode.imageUrl); - dbEpisode.modify()->setDuration(episode.duration); + addEpisode(session, dbPodcast, episode); } } } // namespace diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp index 3d12ba29..8682f79c 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.cpp +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -32,35 +32,35 @@ namespace lms::recommendation { namespace { - db::ScanSettings::RecommendationEngineType getRecommendationEngineType(db::Session& session) + db::RecommendationEngineType getRecommendationEngineType(db::Session& session) { auto transaction{ session.createReadTransaction() }; return db::ScanSettings::find(session)->getRecommendationEngineType(); } - EngineType toEngineType(db::ScanSettings::RecommendationEngineType type) + EngineType toEngineType(db::RecommendationEngineType type) { switch (type) { - case db::ScanSettings::RecommendationEngineType::None: + case db::RecommendationEngineType::None: return EngineType::None; - case db::ScanSettings::RecommendationEngineType::Clusters: + case db::RecommendationEngineType::Clusters: return EngineType::Clusters; - case db::ScanSettings::RecommendationEngineType::AudioSimilarity: + case db::RecommendationEngineType::AudioSimilarity: return EngineType::AudioSimilarity; } return EngineType::None; } - std::unique_ptr createEngine(db::ScanSettings::RecommendationEngineType type, db::IDb& db) + std::unique_ptr createEngine(db::RecommendationEngineType type, db::IDb& db) { switch (type) { - case db::ScanSettings::RecommendationEngineType::Clusters: + case db::RecommendationEngineType::Clusters: return std::make_unique(db); - case db::ScanSettings::RecommendationEngineType::AudioSimilarity: + case db::RecommendationEngineType::AudioSimilarity: return std::make_unique(db); - case db::ScanSettings::RecommendationEngineType::None: + case db::RecommendationEngineType::None: return nullptr; } return nullptr; @@ -139,7 +139,7 @@ namespace lms::recommendation return false; } - db::ScanSettings::RecommendationEngineType RecommendationService::prepareReload() + db::RecommendationEngineType RecommendationService::prepareReload() { const auto type{ getRecommendationEngineType(_db.getTLSSession()) }; std::unique_lock lock{ _mutex }; diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp index 2caeea70..699ef7ac 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.hpp +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -59,7 +59,7 @@ namespace lms::recommendation ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet linkTypes, std::size_t maxCount) const override; TrackResults findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const override; - db::ScanSettings::RecommendationEngineType prepareReload(); + db::RecommendationEngineType prepareReload(); db::IDb& _db; mutable std::shared_mutex _mutex; diff --git a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp index 114335ef..1dc6b7d7 100644 --- a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp +++ b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp @@ -627,13 +627,13 @@ namespace lms::recommendation { db::Release::FindParameters params; params.setArtist(artist->getId()); - for (const db::ReleaseId releaseId : db::Release::findIds(session, params).results) + for (const db::ReleaseId releaseId : db::Release::findIds(session, params)) { if (_releaseVectors.contains(releaseId)) { db::Track::FindParameters trackParams; trackParams.setRelease(releaseId); - for (const db::TrackId trackId : db::Track::findIds(session, trackParams).results) + for (const db::TrackId trackId : db::Track::findIds(session, trackParams)) artistTrackIds.insert(trackId); } } diff --git a/src/libs/services/recommendation/impl/tags/TagsEngine.cpp b/src/libs/services/recommendation/impl/tags/TagsEngine.cpp index 6d1d87c6..3eda4ef0 100644 --- a/src/libs/services/recommendation/impl/tags/TagsEngine.cpp +++ b/src/libs/services/recommendation/impl/tags/TagsEngine.cpp @@ -169,11 +169,11 @@ namespace lms::recommendation db::Release::FindParameters params; params.setArtist(artist->getId()); - for (const db::ReleaseId releaseId : db::Release::findIds(session, params).results) + for (const db::ReleaseId releaseId : db::Release::findIds(session, params)) { db::Track::FindParameters trackParams; trackParams.setRelease(releaseId); - for (const db::TrackId trackId : db::Track::findIds(session, trackParams).results) + for (const db::TrackId trackId : db::Track::findIds(session, trackParams)) artistTrackIds.insert(trackId); } } diff --git a/src/libs/services/scanner/bench/CMakeLists.txt b/src/libs/services/scanner/bench/CMakeLists.txt index 78d75165..d939cb1d 100644 --- a/src/libs/services/scanner/bench/CMakeLists.txt +++ b/src/libs/services/scanner/bench/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(bench-scanner + IgnoreRules.cpp Lyrics.cpp Scanner.cpp TrackMetadataParser.cpp diff --git a/src/libs/services/scanner/bench/IgnoreRules.cpp b/src/libs/services/scanner/bench/IgnoreRules.cpp new file mode 100644 index 00000000..a5c2ee74 --- /dev/null +++ b/src/libs/services/scanner/bench/IgnoreRules.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2025 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 + +#include + +#include "IgnoreRules.hpp" + +namespace lms::scanner::benchmarks +{ + namespace + { + std::filesystem::path makeDeepPath(std::size_t depth) + { + std::filesystem::path path; + for (std::size_t i{}; i < depth; ++i) + path /= "artist_" + std::to_string(i); + + path /= "track.flac"; + return path; + } + + std::string makeManyRules(std::size_t count) + { + std::string content; + for (std::size_t i{}; i < count; ++i) + content += "*.rule_" + std::to_string(i) + "\n"; + + return content; + } + + void BM_IgnoreRules_isIgnored_ShallowNoMatch(benchmark::State& state) + { + const IgnoreRules rules{ "*.nfo\ncovers/\n" }; + + for (auto _ : state) + benchmark::DoNotOptimize(rules.isIgnored("track.flac", IgnoreRules::IsDirectory{ false })); + } + + void BM_IgnoreRules_isIgnored_ShallowMatch(benchmark::State& state) + { + const IgnoreRules rules{ "*.nfo\ncovers/\n" }; + + for (auto _ : state) + benchmark::DoNotOptimize(rules.isIgnored("track.nfo", IgnoreRules::IsDirectory{ false })); + } + + // Worst case for the ancestor walk: no rule ever matches, so every + // ancestor of a deep path gets fully evaluated on every call. + void BM_IgnoreRules_isIgnored_DeepPath_NoMatch(benchmark::State& state) + { + const IgnoreRules rules{ "*.nfo\ncovers/\n" }; + const std::filesystem::path path{ makeDeepPath(static_cast(state.range(0))) }; + + for (auto _ : state) + benchmark::DoNotOptimize(rules.isIgnored(path, IgnoreRules::IsDirectory{ false })); + } + + // Best case: the leaf itself matches, so the ancestor walk exits on its first iteration. + void BM_IgnoreRules_isIgnored_DeepPath_LeafMatch(benchmark::State& state) + { + const IgnoreRules rules{ "*.flac\n" }; + const std::filesystem::path path{ makeDeepPath(static_cast(state.range(0))) }; + + for (auto _ : state) + benchmark::DoNotOptimize(rules.isIgnored(path, IgnoreRules::IsDirectory{ false })); + } + + // Combines a deep path with a large rule set: every level of the + // ancestor walk pays the full per-rule fnmatch fold. + void BM_IgnoreRules_isIgnored_DeepPath_ManyRules_NoMatch(benchmark::State& state) + { + const IgnoreRules rules{ makeManyRules(static_cast(state.range(1))) }; + const std::filesystem::path path{ makeDeepPath(static_cast(state.range(0))) }; + + for (auto _ : state) + benchmark::DoNotOptimize(rules.isIgnored(path, IgnoreRules::IsDirectory{ false })); + } + } // namespace + + BENCHMARK(BM_IgnoreRules_isIgnored_ShallowNoMatch); + BENCHMARK(BM_IgnoreRules_isIgnored_ShallowMatch); + BENCHMARK(BM_IgnoreRules_isIgnored_DeepPath_NoMatch)->Arg(4)->Arg(8); + BENCHMARK(BM_IgnoreRules_isIgnored_DeepPath_LeafMatch)->Arg(4)->Arg(8); + BENCHMARK(BM_IgnoreRules_isIgnored_DeepPath_ManyRules_NoMatch)->Args({ 4, 10 })->Args({ 8, 20 }); + +} // namespace lms::scanner::benchmarks diff --git a/src/libs/services/scanner/impl/IgnoreRules.cpp b/src/libs/services/scanner/impl/IgnoreRules.cpp index b0d7d9d2..81e347af 100644 --- a/src/libs/services/scanner/impl/IgnoreRules.cpp +++ b/src/libs/services/scanner/impl/IgnoreRules.cpp @@ -21,6 +21,7 @@ #include +#include #include namespace lms::scanner @@ -79,9 +80,28 @@ namespace lms::scanner bool IgnoreRules::isIgnored(const std::filesystem::path& relativePath, IsDirectory isDir) const { + assert(relativePath.is_relative()); + if (_rules.empty()) return false; + std::filesystem::path currentPath{ relativePath }; + IsDirectory currentIsDir{ isDir }; + + while (!currentPath.empty()) + { + if (matchesRules(currentPath, currentIsDir)) + return true; + + currentPath = currentPath.parent_path(); + currentIsDir = IsDirectory{ true }; + } + + return false; + } + + bool IgnoreRules::matchesRules(const std::filesystem::path& relativePath, IsDirectory isDir) const + { bool ignored{}; for (const Rule& rule : _rules) { diff --git a/src/libs/services/scanner/impl/IgnoreRules.hpp b/src/libs/services/scanner/impl/IgnoreRules.hpp index 1cef472a..52699829 100644 --- a/src/libs/services/scanner/impl/IgnoreRules.hpp +++ b/src/libs/services/scanner/impl/IgnoreRules.hpp @@ -56,6 +56,8 @@ namespace lms::scanner bool operator==(const Rule&) const = default; }; + bool matchesRules(const std::filesystem::path& relativePath, IsDirectory isDir) const; + std::vector _rules; public: diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index d1ce1a5e..c385b392 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -126,7 +126,7 @@ namespace lms::scanner settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge(); settings->artistImageFallbackToRelease = scanSettings->getArtistImageFallbackToRelease(); - settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::ScanSettings::RecommendationEngineType::AudioSimilarity; + settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::RecommendationEngineType::AudioSimilarity; settings->musicnnModelPath = core::Service::get()->getPath("musicnn-model-path", "/usr/share/lms/models/MSD_musicnn_embedding.onnx"); settings->musicnnMaxPatchCountPerTrack = core::Service::get()->getULong("musicnn-max-patch-count-per-track", 20); diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 9f7b5918..7e68b47a 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -41,6 +41,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" @@ -49,6 +50,7 @@ #include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackMusicNNEmbeddings.hpp" +#include "database/objects/Work.hpp" #include "image/Exception.hpp" #include "image/Image.hpp" @@ -368,6 +370,24 @@ namespace lms::scanner return moods; } + std::vector getOrCreateWorks(db::Session& session, db::ReleaseId releaseId, std::span works) + { + std::vector dbWorks; + dbWorks.reserve(works.size()); + for (const Work& work : works) + { + // Work titles are often generic and collide across unrelated works, so + // without an mbid we only ever match a work already used on the same release, not globally by name + db::Work::pointer dbWork{ work.mbid ? db::Work::find(session, *work.mbid) : (releaseId.isValid() ? db::Work::find(session, releaseId, work.name) : db::Work::pointer{}) }; + if (!dbWork) + dbWork = session.create(work.name, work.mbid); + else if (dbWork->getName() != work.name) + dbWork.modify()->setName(work.name); + dbWorks.push_back(dbWork); + } + return dbWorks; + } + std::vector getOrCreateClusters(db::Session& session, const Track& track) { std::vector clusters; @@ -836,6 +856,12 @@ namespace lms::scanner track.modify()->setGroupings(getOrCreateGroupings(dbSession, _file->track.groupings)); track.modify()->setLanguages(getOrCreateLanguages(dbSession, _file->track.languages)); track.modify()->setMoods(getOrCreateMoods(dbSession, _file->track.moods)); + track.modify()->setWorks(getOrCreateWorks(dbSession, track->getReleaseId(), _file->track.works)); + + track.modify()->clearMovements(); + for (const auto& movement : _file->track.movements) + db::Movement::create(dbSession, movement.name, movement.number, movement.count, track); + track.modify()->setName(title); track.modify()->setTrackNumber(_file->track.position); track.modify()->setDate(_file->track.date); diff --git a/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp index 230d2e30..c248a6f5 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/TrackMetadataParser.cpp @@ -184,6 +184,38 @@ namespace lms::scanner return res; } + std::vector getWorks(const audio::ITagReader& tagReader) + { + const std::vector titles{ getTagValuesAs(tagReader, audio::TagType::WorkTitle, {}) }; + const std::vector mbids{ getTagValuesAs(tagReader, audio::TagType::MusicBrainzWorkID, {}) }; + + const bool mbidsMatch{ mbids.size() == titles.size() }; + + std::vector works; + works.reserve(titles.size()); + for (std::size_t i{}; i < titles.size(); ++i) + works.push_back({ mbidsMatch ? std::optional{ mbids[i] } : std::nullopt, titles[i] }); + + return works; + } + + std::vector getMovements(const audio::ITagReader& tagReader) + { + const std::vector names{ getTagValuesAs(tagReader, audio::TagType::Movement, {}) }; + const std::vector numbers{ getTagValuesAs(tagReader, audio::TagType::MovementNumber, {}) }; + const std::vector counts{ getTagValuesAs(tagReader, audio::TagType::MovementCount, {}) }; + + const bool numbersMatch{ numbers.size() == names.size() }; + const bool countsMatch{ counts.size() == names.size() }; + + std::vector movements; + movements.reserve(names.size()); + for (std::size_t i{}; i < names.size(); ++i) + movements.push_back({ names[i], numbersMatch ? std::optional{ numbers[i] } : std::nullopt, countsMatch ? std::optional{ counts[i] } : std::nullopt }); + + return movements; + } + std::vector getArtists(const audio::ITagReader& tagReader, std::initializer_list artistTagNames, std::initializer_list artistSortTagNames, @@ -328,6 +360,8 @@ namespace lms::scanner track.title = getTagValueAs(tagReader, TagType::TrackTitle).value_or(""); track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID); track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID); + track.works = getWorks(tagReader); + track.movements = getMovements(tagReader); track.acoustID = getTagValueAs(tagReader, TagType::AcoustID); track.position = getTagValueAs(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine if (const auto dateStr{ getTagValueAs(tagReader, TagType::Date) }) diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp index 83d036b6..bedce282 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp @@ -183,10 +183,10 @@ namespace lms::scanner auto transaction{ session.createReadTransaction() }; const auto artists{ db::Artist::findWithMBIDNameVariants(session, lastRetrievedArtist, db::Range{ .offset = 0, .size = batchSize }) }; - if (artists.results.empty()) + if (artists.empty()) break; - for (const db::Artist::pointer& artist : artists.results) + for (const db::Artist::pointer& artist : artists) { bool hasArtistInfo{}; db::ArtistInfo::find(session, artist->getId(), db::Range{ .offset = 0, .size = 1 }, [&](const db::ArtistInfo::pointer&) { @@ -218,7 +218,7 @@ namespace lms::scanner } } - if (!artists.moreResults) + if (artists.size() < batchSize) break; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp index 8c3aafae..266f980d 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForDuplicatedFiles.cpp @@ -41,8 +41,8 @@ namespace lms::scanner Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - const RangeResults tracks = Track::findIdsTrackMBIDDuplicates(session); - for (const TrackId trackId : tracks.results) + const std::vector tracks = Track::findIdsTrackMBIDDuplicates(session); + for (const TrackId trackId : tracks) { if (_abortScan) break; diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp index aef28aa0..07c7c100 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp @@ -19,6 +19,7 @@ #include "ScanStepCheckForRemovedFiles.hpp" +#include #include #include #include @@ -107,17 +108,22 @@ namespace lms::scanner return false; } - const auto isInActiveLibrary{ [&](const MediaLibraryInfo& lib) { - if (!core::pathUtils::isPathInRootPath(p, lib.rootDirectory)) - return false; - return lib.ignoreRules.isEmpty() || !lib.ignoreRules.isIgnored(std::filesystem::relative(p, lib.rootDirectory), IgnoreRules::IsDirectory{ false }); - } }; - if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries), isInActiveLibrary)) + // media library root paths never overlap: at most one library can own this path + const auto itOwningLibrary{ std::find_if(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries), [&](const MediaLibraryInfo& lib) { + return core::pathUtils::isPathInRootPath(p, lib.rootDirectory); + }) }; + if (itOwningLibrary == std::cend(_settings.mediaLibraries)) { LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": out of media directory"); return false; } + if (itOwningLibrary->ignoreRules.isIgnored(std::filesystem::relative(p, itOwningLibrary->rootDirectory), IgnoreRules::IsDirectory{ false })) + { + LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": ignored by .lmsignore rules"); + return false; + } + if (!_scanners.select(p)) { LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": file format no longer handled"); diff --git a/src/libs/services/scanner/impl/steps/ScanStepComputeGenreStats.cpp b/src/libs/services/scanner/impl/steps/ScanStepComputeGenreStats.cpp index be1fdb18..8a1636bd 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepComputeGenreStats.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepComputeGenreStats.cpp @@ -50,7 +50,7 @@ namespace lms::scanner Genre::FindParameters params; params.setRange(range); auto transaction{ dbSession.createReadTransaction() }; - return std::move(Genre::findIds(dbSession, params).results); + return Genre::findIds(dbSession, params); }() }; for (const GenreId genreId : genreIds) @@ -65,6 +65,7 @@ namespace lms::scanner trackCount = Genre::computeTrackCount(dbSession, genreId); releaseCount = Genre::computeReleaseCount(dbSession, genreId); } + { auto transaction{ dbSession.createWriteTransaction() }; auto genre{ Genre::find(dbSession, genreId) }; diff --git a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp index 3eb63d08..df484b4c 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.cpp @@ -33,6 +33,7 @@ #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackEmbeddedImage.hpp" +#include "database/objects/Work.hpp" #include "ScanContext.hpp" @@ -52,6 +53,7 @@ namespace lms::scanner removeOrphanedGroupings(context); removeOrphanedLanguages(context); removeOrphanedMoods(context); + removeOrphanedWorks(context); removeOrphanedArtists(context); removeOrphanedReleases(context); removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key @@ -98,6 +100,12 @@ namespace lms::scanner removeOrphanedEntries(context); } + void ScanStepRemoveOrphanedDbEntries::removeOrphanedWorks(ScanContext& context) + { + LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned works..."); + removeOrphanedEntries(context); + } + void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists(ScanContext& context) { LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists..."); @@ -155,7 +163,7 @@ namespace lms::scanner db::Session& session{ _db.getTLSSession() }; - db::RangeResults entries; + std::vector entries; while (!_abortScan) { { @@ -164,16 +172,16 @@ namespace lms::scanner entries = T::findOrphanIds(session, db::Range{ 0, batchSize }); }; - if (entries.results.empty()) + if (entries.empty()) break; { auto transaction{ session.createWriteTransaction() }; - session.destroy(entries.results); + session.destroy(entries); } - context.currentStepStats.processedElems += entries.results.size(); + context.currentStepStats.processedElems += entries.size(); _progressCallback(context.currentStepStats); } } diff --git a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp index 3173414e..27f59955 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepRemoveOrphanedDbEntries.hpp @@ -40,6 +40,7 @@ namespace lms::scanner void removeOrphanedGroupings(ScanContext& context); void removeOrphanedLanguages(ScanContext& context); void removeOrphanedMoods(ScanContext& context); + void removeOrphanedWorks(ScanContext& context); void removeOrphanedArtists(ScanContext& context); void removeOrphanedMediums(ScanContext& context); void removeOrphanedReleases(ScanContext& context); diff --git a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp index 22500381..b787eeb4 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepUpdateLibraryFields.cpp @@ -58,7 +58,7 @@ namespace lms::scanner constexpr std::size_t batchSize = 100; - db::RangeResults entries; + std::vector entries; while (!_abortScan) { { @@ -67,7 +67,7 @@ namespace lms::scanner entries = db::Directory::findMismatchedLibrary(session, db::Range{ 0, batchSize }, mediaLibrary.rootDirectory, mediaLibrary.id); }; - if (entries.results.empty()) + if (entries.empty()) break; { @@ -77,7 +77,7 @@ namespace lms::scanner if (!library) // may be legit break; - for (const db::DirectoryId directoryId : entries.results) + for (const db::DirectoryId directoryId : entries) { if (_abortScan) break; @@ -87,7 +87,7 @@ namespace lms::scanner } } - context.currentStepStats.processedElems += entries.results.size(); + context.currentStepStats.processedElems += entries.size(); _progressCallback(context.currentStepStats); } } diff --git a/src/libs/services/scanner/impl/types/TrackMetadata.hpp b/src/libs/services/scanner/impl/types/TrackMetadata.hpp index 319c5fd8..ee247f68 100644 --- a/src/libs/services/scanner/impl/types/TrackMetadata.hpp +++ b/src/libs/services/scanner/impl/types/TrackMetadata.hpp @@ -54,6 +54,14 @@ namespace lms::scanner using PerformerContainer = std::map>; + struct Work + { + std::optional mbid; + std::string name; + + auto operator<=>(const Work&) const = default; + }; + struct Release { std::optional mbid; @@ -100,8 +108,17 @@ namespace lms::scanner Clean, }; + struct MovementData + { + std::string name; + std::optional number; + std::optional count; + }; + std::optional mbid; std::optional recordingMBID; + std::vector works; + std::vector movements; std::string title; std::optional medium; std::optional position; // in medium diff --git a/src/libs/services/scanner/test/CMakeLists.txt b/src/libs/services/scanner/test/CMakeLists.txt index b603b4ab..619bcea7 100644 --- a/src/libs/services/scanner/test/CMakeLists.txt +++ b/src/libs/services/scanner/test/CMakeLists.txt @@ -3,7 +3,7 @@ include(GoogleTest) add_executable(test-scanner ArtistInfo.cpp AudioFileUtils.cpp - IgnoreFilter.cpp + IgnoreRules.cpp Lyrics.cpp PlayList.cpp ScannerStats.cpp diff --git a/src/libs/services/scanner/test/IgnoreFilter.cpp b/src/libs/services/scanner/test/IgnoreRules.cpp similarity index 78% rename from src/libs/services/scanner/test/IgnoreFilter.cpp rename to src/libs/services/scanner/test/IgnoreRules.cpp index 920aae65..01155845 100644 --- a/src/libs/services/scanner/test/IgnoreFilter.cpp +++ b/src/libs/services/scanner/test/IgnoreRules.cpp @@ -30,6 +30,13 @@ namespace lms::scanner::tests EXPECT_FALSE(f.isIgnored("track.flac", IgnoreRules::IsDirectory{ false })); } + TEST(IgnoreRules, EmptyPath_NeverIgnored) + { + const IgnoreRules f{ "*.mp3\ncovers/\n" }; + EXPECT_FALSE(f.isIgnored("", IgnoreRules::IsDirectory{ false })); + EXPECT_FALSE(f.isIgnored("", IgnoreRules::IsDirectory{ true })); + } + TEST(IgnoreRules, CommentsAndBlanksOnly) { const IgnoreRules f{ "# this is a comment\n\n# another comment\n" }; @@ -79,7 +86,7 @@ namespace lms::scanner::tests const IgnoreRules f{ "covers*/\n" }; EXPECT_TRUE(f.isIgnored("covers", IgnoreRules::IsDirectory{ true })); EXPECT_TRUE(f.isIgnored("covers_2024", IgnoreRules::IsDirectory{ true })); - EXPECT_TRUE(f.isIgnored("jazz/covers_hq", IgnoreRules::IsDirectory{ true })); // unanchored — matches at any depth + EXPECT_TRUE(f.isIgnored("jazz/covers_hq", IgnoreRules::IsDirectory{ true })); // unanchored: matches at any depth EXPECT_FALSE(f.isIgnored("notcovers", IgnoreRules::IsDirectory{ true })); EXPECT_FALSE(f.isIgnored("covers_2024", IgnoreRules::IsDirectory{ false })); // dirOnly } @@ -112,29 +119,51 @@ namespace lms::scanner::tests TEST(IgnoreRules, DirOnly_AnchoredOnlyMatchesRoot) { const IgnoreRules f{ "/untagged/\n" }; - EXPECT_TRUE(f.isIgnored("untagged", IgnoreRules::IsDirectory{ true })); // root level — match - EXPECT_FALSE(f.isIgnored("jazz/untagged", IgnoreRules::IsDirectory{ true })); // nested — no match - EXPECT_FALSE(f.isIgnored("untagged", IgnoreRules::IsDirectory{ false })); // file, not dir — no match + EXPECT_TRUE(f.isIgnored("untagged", IgnoreRules::IsDirectory{ true })); // root level: match + EXPECT_FALSE(f.isIgnored("jazz/untagged", IgnoreRules::IsDirectory{ true })); // nested: no match + EXPECT_FALSE(f.isIgnored("untagged", IgnoreRules::IsDirectory{ false })); // file, not dir: no match } TEST(IgnoreRules, DirOnly_UnanchoredMatchesAnyDepth) { const IgnoreRules f{ "covers/\n" }; - EXPECT_TRUE(f.isIgnored("covers", IgnoreRules::IsDirectory{ true })); // root — match - EXPECT_TRUE(f.isIgnored("jazz/covers", IgnoreRules::IsDirectory{ true })); // nested — also match - EXPECT_TRUE(f.isIgnored("a/b/c/covers", IgnoreRules::IsDirectory{ true })); // deep — also match - EXPECT_FALSE(f.isIgnored("covers", IgnoreRules::IsDirectory{ false })); // file, not dir — no match + EXPECT_TRUE(f.isIgnored("covers", IgnoreRules::IsDirectory{ true })); // root: match + EXPECT_TRUE(f.isIgnored("jazz/covers", IgnoreRules::IsDirectory{ true })); // nested: also match + EXPECT_TRUE(f.isIgnored("a/b/c/covers", IgnoreRules::IsDirectory{ true })); // deep: also match + EXPECT_FALSE(f.isIgnored("covers", IgnoreRules::IsDirectory{ false })); // file, not dir: no match } TEST(IgnoreRules, FullPath_ExactDir) { const IgnoreRules f{ "jazz/covers\n" }; EXPECT_TRUE(f.isIgnored("jazz/covers", IgnoreRules::IsDirectory{ true })); - EXPECT_FALSE(f.isIgnored("jazz/covers/foo", IgnoreRules::IsDirectory{ true })); // never reached in practice: scanner prunes jazz/covers/ first - EXPECT_FALSE(f.isIgnored("foo/jazz/covers", IgnoreRules::IsDirectory{ true })); // never reached in practice: scanner prunes jazz/covers/ first + EXPECT_TRUE(f.isIgnored("jazz/covers/foo", IgnoreRules::IsDirectory{ true })); // descendant of an ignored dir + EXPECT_FALSE(f.isIgnored("foo/jazz/covers", IgnoreRules::IsDirectory{ true })); // unrelated path structure, no prefix matches EXPECT_FALSE(f.isIgnored("rock/covers", IgnoreRules::IsDirectory{ true })); } + TEST(IgnoreRules, AncestorDirIgnored_MatchesDescendantFile) + { + const IgnoreRules f{ "import\n" }; + EXPECT_TRUE(f.isIgnored("import/song.mp3", IgnoreRules::IsDirectory{ false })); + EXPECT_TRUE(f.isIgnored("import/sub/song.mp3", IgnoreRules::IsDirectory{ false })); + EXPECT_FALSE(f.isIgnored("other/song.mp3", IgnoreRules::IsDirectory{ false })); + } + + TEST(IgnoreRules, AncestorDirIgnored_AnchoredRuleMatchesNestedFile) + { + const IgnoreRules f{ "/import/\n" }; + EXPECT_TRUE(f.isIgnored("import/song.mp3", IgnoreRules::IsDirectory{ false })); + EXPECT_FALSE(f.isIgnored("jazz/import/song.mp3", IgnoreRules::IsDirectory{ false })); // anchored to root only + } + + TEST(IgnoreRules, AncestorDirIgnored_NegationDoesNotReinclude) + { + const IgnoreRules f{ "jazz/\n!jazz/keep.flac\n" }; + EXPECT_TRUE(f.isIgnored("jazz/keep.flac", IgnoreRules::IsDirectory{ false })); // ancestor dir ignored: negation deeper inside has no effect + EXPECT_TRUE(f.isIgnored("jazz", IgnoreRules::IsDirectory{ true })); + } + TEST(IgnoreRules, FullPath_GlobInDir) { const IgnoreRules f{ "jazz/*.nfo\n" }; diff --git a/src/libs/services/scanner/test/TestTagReader.hpp b/src/libs/services/scanner/test/TestTagReader.hpp index 3ba949a0..d5ed5e1b 100644 --- a/src/libs/services/scanner/test/TestTagReader.hpp +++ b/src/libs/services/scanner/test/TestTagReader.hpp @@ -137,6 +137,11 @@ namespace lms::scanner::tests { TagType::MusicBrainzReleaseID, { "3fa39992-b786-4585-a70e-85d5cc15ef69" } }, { TagType::MusicBrainzReleaseGroupID, { "5b1a5a44-8420-4426-9b86-d25dc8d04838" } }, { TagType::MusicBrainzRecordingID, { "bd3fc666-89de-4ac8-93f6-2dbf028ad8d5" } }, + { TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888", "aaaabbbb-cccc-dddd-eeee-ffff00001111" } }, + { TagType::WorkTitle, { "MyWork1", "MyWork2" } }, + { TagType::Movement, { "Allegro con brio", "Andante" } }, + { TagType::MovementNumber, { "1", "2" } }, + { TagType::MovementCount, { "4", "4" } }, { TagType::Producer, { "MyProducer1", "MyProducer2" } }, { TagType::Remixer, { "MyRemixer1", "MyRemixer2" } }, { TagType::RecordLabel, { "Label1", "Label2" } }, diff --git a/src/libs/services/scanner/test/TrackMetadataParser.cpp b/src/libs/services/scanner/test/TrackMetadataParser.cpp index 6f94bf4b..7ea182a0 100644 --- a/src/libs/services/scanner/test/TrackMetadataParser.cpp +++ b/src/libs/services/scanner/test/TrackMetadataParser.cpp @@ -114,6 +114,24 @@ namespace lms::scanner::tests EXPECT_EQ(track.producerArtists[1].name, "MyProducer2"); ASSERT_TRUE(track.recordingMBID.has_value()); EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5")); + ASSERT_EQ(track.works.size(), 2); + EXPECT_EQ(track.works[0].name, "MyWork1"); + EXPECT_EQ(track.works[1].name, "MyWork2"); + ASSERT_TRUE(track.works[0].mbid.has_value()); + EXPECT_EQ(track.works[0].mbid.value(), core::UUID::fromString("11112222-3333-4444-5555-666677778888")); + ASSERT_TRUE(track.works[1].mbid.has_value()); + EXPECT_EQ(track.works[1].mbid.value(), core::UUID::fromString("aaaabbbb-cccc-dddd-eeee-ffff00001111")); + ASSERT_EQ(track.movements.size(), 2); + EXPECT_EQ(track.movements[0].name, "Allegro con brio"); + ASSERT_TRUE(track.movements[0].number.has_value()); + EXPECT_EQ(track.movements[0].number.value(), 1); + ASSERT_TRUE(track.movements[0].count.has_value()); + EXPECT_EQ(track.movements[0].count.value(), 4); + EXPECT_EQ(track.movements[1].name, "Andante"); + ASSERT_TRUE(track.movements[1].number.has_value()); + EXPECT_EQ(track.movements[1].number.value(), 2); + ASSERT_TRUE(track.movements[1].count.has_value()); + EXPECT_EQ(track.movements[1].count.value(), 4); ASSERT_TRUE(track.replayGain.has_value()); EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33); ASSERT_EQ(track.remixerArtists.size(), 2); @@ -720,6 +738,60 @@ namespace lms::scanner::tests EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name } + TEST(TrackMetadataParser, works_allMbidsPresent) + { + const TestTagReader testTags{ + { + { audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } }, + { audio::TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888", "aaaabbbb-cccc-dddd-eeee-ffff00001111" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.works.size(), 2); + EXPECT_EQ(track.works[0].name, "MyWork1"); + EXPECT_EQ(track.works[0].mbid, core::UUID::fromString("11112222-3333-4444-5555-666677778888")); + EXPECT_EQ(track.works[1].name, "MyWork2"); + EXPECT_EQ(track.works[1].mbid, core::UUID::fromString("aaaabbbb-cccc-dddd-eeee-ffff00001111")); + } + + TEST(TrackMetadataParser, works_noMbids) + { + const TestTagReader testTags{ + { + { audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.works.size(), 2); + EXPECT_EQ(track.works[0].name, "MyWork1"); + EXPECT_EQ(track.works[0].mbid, std::nullopt); + EXPECT_EQ(track.works[1].name, "MyWork2"); + EXPECT_EQ(track.works[1].mbid, std::nullopt); + } + + TEST(TrackMetadataParser, works_mismatchedMbidCount) + { + // mbid count does not match work title count => mbids are all discarded (all or nothing) + const TestTagReader testTags{ + { + { audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } }, + { audio::TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888" } }, + } + }; + + const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) }; + + ASSERT_EQ(track.works.size(), 2); + EXPECT_EQ(track.works[0].name, "MyWork1"); + EXPECT_EQ(track.works[0].mbid, std::nullopt); + EXPECT_EQ(track.works[1].name, "MyWork2"); + EXPECT_EQ(track.works[1].mbid, std::nullopt); + } + TEST(TrackMetadataParser, heterogeneousArtistMbids) { { diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index a03c0069..21890ab5 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -156,17 +156,9 @@ namespace lms::scrobbling ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(const ArtistFindParameters& params) { ArtistContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::ArtistStatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getRecentArtists(session, listenFindParams); return res; } @@ -174,17 +166,9 @@ namespace lms::scrobbling ScrobblingService::ReleaseContainer ScrobblingService::getRecentReleases(const FindParameters& params) { ReleaseContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::StatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getRecentReleases(session, listenFindParams); return res; } @@ -192,17 +176,9 @@ namespace lms::scrobbling ScrobblingService::TrackContainer ScrobblingService::getRecentTracks(const FindParameters& params) { TrackContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::StatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getRecentTracks(session, listenFindParams); return res; } @@ -243,17 +219,9 @@ namespace lms::scrobbling ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(const ArtistFindParameters& params) { ArtistContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::ArtistStatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getTopArtists(session, listenFindParams); return res; } @@ -261,17 +229,9 @@ namespace lms::scrobbling ScrobblingService::ReleaseContainer ScrobblingService::getTopReleases(const FindParameters& params) { ReleaseContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::StatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getTopReleases(session, listenFindParams); return res; } @@ -279,17 +239,9 @@ namespace lms::scrobbling ScrobblingService::TrackContainer ScrobblingService::getTopTracks(const FindParameters& params) { TrackContainer res; - - const auto backend{ getUserBackend(params.user) }; - if (!backend) - return res; - db::Listen::StatsFindParameters listenFindParams{ convertToListenFindParameters(params) }; - listenFindParams.setScrobblingBackend(backend); - Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; - res = db::Listen::getTopTracks(session, listenFindParams); return res; } diff --git a/src/libs/services/scrobbling/impl/lastfm/ScrobblingsSynchronizer.cpp b/src/libs/services/scrobbling/impl/lastfm/ScrobblingsSynchronizer.cpp index 07e7751c..50f7e4e7 100644 --- a/src/libs/services/scrobbling/impl/lastfm/ScrobblingsSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/lastfm/ScrobblingsSynchronizer.cpp @@ -226,8 +226,8 @@ namespace lms::scrobbling::lastFm .setSyncState(db::SyncState::PendingAdd) .setRange(db::Range{ 0, maxBatchSize * 10 }); - const db::RangeResults results{ db::Listen::find(session, params) }; - for (const db::ListenId listenId : results.results) + const auto results{ db::Listen::find(session, params) }; + for (const db::ListenId listenId : results) { const db::Listen::pointer dbListen{ db::Listen::find(session, listenId) }; diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index 9c20ddeb..0f36d7e2 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -118,7 +118,7 @@ namespace lms::scrobbling::listenBrainz Wt::Json::Object trackMetadata; trackMetadata["additional_info"] = std::move(additionalInfo); trackMetadata["artist_name"] = Wt::Json::Value{ std::string{ track->getArtistDisplayName() } }; - trackMetadata["track_name"] = Wt::Json::Value{ track->getName() }; + trackMetadata["track_name"] = Wt::Json::Value{ std::string{ track->getName() } }; if (track->getRelease()) trackMetadata["release_name"] = Wt::Json::Value{ std::string{ track->getRelease()->getName() } }; @@ -214,12 +214,12 @@ namespace lms::scrobbling::listenBrainz const auto tracks{ Track::findIds(session, params) }; // conservative behavior: in case of multiple matches: reject - if (tracks.results.size() == 1) + if (tracks.size() == 1) { LMS_LOG_LISTENBRAINZ(DEBUG, "Matched listen '" << listen << "' using metadata"); - return tracks.results.front(); + return tracks.front(); } - else if (tracks.results.size() > 1) + else if (tracks.size() > 1) { LMS_LOG_LISTENBRAINZ(DEBUG, "Too many matches for listen '" << listen << "' using metadata"); return {}; @@ -349,10 +349,10 @@ namespace lms::scrobbling::listenBrainz .setSyncState(db::SyncState::PendingAdd) .setRange(db::Range{ 0, 100 }); // don't flood too much? - const db::RangeResults results{ db::Listen::find(session, params) }; - pendingListens.reserve(results.results.size()); + const auto results{ db::Listen::find(session, params) }; + pendingListens.reserve(results.size()); - for (db::ListenId listenId : results.results) + for (db::ListenId listenId : results) { const db::Listen::pointer listen{ db::Listen::find(session, listenId) }; @@ -421,14 +421,14 @@ namespace lms::scrobbling::listenBrainz enquePendingListens(); - db::RangeResults userIds; + std::vector userIds; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; userIds = db::User::find(_db.getTLSSession(), db::User::FindParameters{}.setScrobblingBackend(db::ScrobblingBackend::ListenBrainz)); } - for (const db::UserId userId : userIds.results) + for (const db::UserId userId : userIds) startSync(getUserContext(userId)); if (!isSyncing()) diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index f7af0d49..540173e4 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -66,9 +66,9 @@ namespace lms::scrobbling virtual void visitNowPlayingListens(const std::function& visitor, db::UserId userId = {}) = 0; // Stats - using ArtistContainer = db::RangeResults; - using ReleaseContainer = db::RangeResults; - using TrackContainer = db::RangeResults; + using ArtistContainer = std::vector; + using ReleaseContainer = std::vector; + using TrackContainer = std::vector; struct FindParameters { diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index c3c27e6e..077d7eba 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmssubsonic STATIC + impl/AuthUtils.cpp impl/endpoints/transcoding/AudioFileInfo.cpp impl/endpoints/transcoding/TranscodeDecision.cpp impl/endpoints/transcoding/TranscodeDecisionTracker.cpp diff --git a/src/libs/subsonic/bench/SubsonicBench.cpp b/src/libs/subsonic/bench/SubsonicBench.cpp index eee953fd..93eaa8b5 100644 --- a/src/libs/subsonic/bench/SubsonicBench.cpp +++ b/src/libs/subsonic/bench/SubsonicBench.cpp @@ -29,7 +29,7 @@ namespace lms::api::subsonic::benchs { Response generateFakeResponse() { - Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + Response response{ Response::createOkResponse() }; Response::Node& node{ response.createNode("MyNode") }; node.setAttribute("Attr1", "value1"); diff --git a/src/libs/subsonic/impl/AuthUtils.cpp b/src/libs/subsonic/impl/AuthUtils.cpp new file mode 100644 index 00000000..50b0393d --- /dev/null +++ b/src/libs/subsonic/impl/AuthUtils.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2026 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 "AuthUtils.hpp" + +#include + +#include + +#include "core/Md5.hpp" +#include "core/Service.hpp" +#include "core/String.hpp" +#include "core/Utils.hpp" + +#include "database/Session.hpp" +#include "services/auth/IAuthTokenService.hpp" + +#include "ParameterParsing.hpp" +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic::utils +{ + namespace + { + // Finds, among the user's stored "subsonic" API keys, the one that satisfies the legacy "t"+"s" scheme, if any (empty string if none) + std::string findMatchingApiKeyForAuthToken(db::UserId userId, std::string_view salt, std::string_view token) + { + std::string matchedApiKey; + + core::Service::get()->visitAuthTokens("subsonic", userId, [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view apiKey) { + if (matchedApiKey.empty() && checkAuthToken(apiKey, salt, token)) + matchedApiKey = apiKey; + }); + + return matchedApiKey; + } + + // Returns null if no such user exists + db::User::pointer getUserFromLoginName(db::Session& session, std::string_view loginName) + { + auto transaction{ session.createReadTransaction() }; + + return db::User::find(session, loginName); + } + } // namespace + + AuthenticationRequest parseAndValidateAuthenticationRequest(const Wt::Http::ParameterMap& parameters) + { + const std::optional user{ getParameterAs(parameters, "u") }; + const std::optional password{ getParameterAs(parameters, "p") }; + const std::optional token{ getParameterAs(parameters, "t") }; + const std::optional salt{ getParameterAs(parameters, "s") }; + const std::optional apiKey{ getParameterAs(parameters, "apiKey") }; + + const bool passwordAuthRequested{ password.has_value() }; + const bool tokenAuthRequested{ token.has_value() || salt.has_value() }; + + if (passwordAuthRequested && tokenAuthRequested) + throw MultipleConflictingAuthenticationMechanismsProvidedError{}; + + if (tokenAuthRequested) + { + if (!user) + throw RequiredParameterMissingError{ "u" }; + if (!token) + throw RequiredParameterMissingError{ "t" }; + if (!salt) + throw RequiredParameterMissingError{ "s" }; + if (apiKey) + throw MultipleConflictingAuthenticationMechanismsProvidedError{}; + return TokenAuthentication{ .user = *user, .token = *token, .salt = *salt }; + } + + if (passwordAuthRequested) + { + if (!user) + throw RequiredParameterMissingError{ "u" }; + if (apiKey) + throw MultipleConflictingAuthenticationMechanismsProvidedError{}; + return PasswordAuthentication{ .user = *user, .password = *password }; + } + + if (user) + throw RequiredParameterMissingError{ "p" }; + if (!apiKey) + throw RequiredParameterMissingError{ "apiKey" }; + + return ApiKeyAuthentication{ .apiKey = *apiKey }; + } + + bool checkAuthToken(std::string_view apiKey, std::string_view salt, std::string_view token) + { + std::string payload; + payload.reserve(apiKey.size() + salt.size()); + payload += apiKey; + payload += salt; + + return core::stringUtils::stringCaseInsensitiveEqual(core::stringUtils::bufferToHexString(core::md5(payload)), token); + } + + db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId) + { + auto transaction{ session.createReadTransaction() }; + + if (db::User::pointer user{ db::User::find(session, userId) }) + return user; + + throw UserNotAuthorizedError{}; + } + + db::UserId authenticateUser(const Wt::Http::Request& request, db::Session& session) + { + const AuthenticationRequest authRequest{ parseAndValidateAuthenticationRequest(request.getParameterMap()) }; + + const auto clientAddress{ boost::asio::ip::make_address(request.clientAddress()) }; + auto& authTokenService{ *core::Service::get() }; + + const std::string authToken{ + std::visit(core::utils::overloads{ + [&](const PasswordAuthentication& auth) { + return decodePasswordIfNeeded(auth.password); + }, + [&](const ApiKeyAuthentication& auth) { + return auth.apiKey; + }, + [&](const TokenAuthentication& auth) { + if (authTokenService.isClientThrottled(clientAddress)) + throw LoginThrottledGenericError{}; + + const db::User::pointer authUser{ getUserFromLoginName(session, auth.user) }; + return authUser ? findMatchingApiKeyForAuthToken(authUser->getId(), auth.salt, auth.token) : std::string{}; + }, + }, + authRequest) + }; + + // It is OK to have an empty authToken string here, as the auth service will reject and counts this as a bad attempt for this client address + const auto authResult{ authTokenService.processAuthToken("subsonic", clientAddress, authToken) }; + + switch (authResult.state) + { + case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted: + // Only the password mechanism needs this check as the token mechanism already used the user name + if (const auto* passwordAuth{ std::get_if(&authRequest) }) + { + const auto authenticatedUser{ getUserFromUserId(session, authResult.authTokenInfo->userId) }; + if (!authenticatedUser || authenticatedUser->getLoginName() != passwordAuth->user) + throw WrongUsernameOrPasswordError{}; + } + return authResult.authTokenInfo->userId; + case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: + if (std::holds_alternative(authRequest)) + throw InvalidAPIkeyError{}; + else + throw WrongUsernameOrPasswordError{}; + case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: + throw LoginThrottledGenericError{}; + } + + throw InternalErrorGenericError{ "Cannot authenticate user" }; + } +} // namespace lms::api::subsonic::utils diff --git a/src/libs/subsonic/impl/AuthUtils.hpp b/src/libs/subsonic/impl/AuthUtils.hpp new file mode 100644 index 00000000..adb67220 --- /dev/null +++ b/src/libs/subsonic/impl/AuthUtils.hpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2026 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 + +#include + +#include "database/objects/User.hpp" +#include "database/objects/UserId.hpp" + +namespace lms::db +{ + class Session; +} + +namespace lms::api::subsonic::utils +{ + struct PasswordAuthentication + { + std::string user; + std::string password; + }; + + struct TokenAuthentication + { + std::string user; + std::string token; + std::string salt; + }; + + struct ApiKeyAuthentication + { + std::string apiKey; + }; + + using AuthenticationRequest = std::variant; + + // Throws on error + AuthenticationRequest parseAndValidateAuthenticationRequest(const Wt::Http::ParameterMap& parameters); + + // Checks token == hex(md5(apiKey + salt)), case insensitive + bool checkAuthToken(std::string_view apiKey, std::string_view salt, std::string_view token); + + // Throws UserNotAuthorizedError if no such user exists + db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId); + + // Throws on error + db::UserId authenticateUser(const Wt::Http::Request& request, db::Session& session); +} // namespace lms::api::subsonic::utils diff --git a/src/libs/subsonic/impl/ProtocolVersion.hpp b/src/libs/subsonic/impl/ProtocolVersion.hpp index 67f4f378..8ac7d99a 100644 --- a/src/libs/subsonic/impl/ProtocolVersion.hpp +++ b/src/libs/subsonic/impl/ProtocolVersion.hpp @@ -33,7 +33,7 @@ namespace lms::api::subsonic unsigned patch{}; }; - static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 1 }; + static inline constexpr ProtocolVersion serverProtocolVersion{ 1, 16, 1 }; } // namespace lms::api::subsonic namespace lms::core::stringUtils diff --git a/src/libs/subsonic/impl/RequestContext.cpp b/src/libs/subsonic/impl/RequestContext.cpp index 755aac4b..723a86b4 100644 --- a/src/libs/subsonic/impl/RequestContext.cpp +++ b/src/libs/subsonic/impl/RequestContext.cpp @@ -50,10 +50,9 @@ namespace lms::api::subsonic , _clientName{ getMandatoryParameterAs(_request.getParameterMap(), "c") } , _clientProtocolVersion{ getMandatoryParameterAs(_request.getParameterMap(), "v") } , _responseFormat{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml } - , _serverProtocolVersion{ _config.serverProtocolVersionsByClient.contains(_clientName) ? _config.serverProtocolVersionsByClient.at(_clientName) : defaultServerProtocolVersion } , _isOpenSubsonicEnabled{ !_config.openSubsonicDisabledClients.contains(_clientName) } { - checkProtocolVersion(_clientProtocolVersion, _serverProtocolVersion); + checkProtocolVersion(_clientProtocolVersion, serverProtocolVersion); } RequestContext::~RequestContext() = default; @@ -93,11 +92,6 @@ namespace lms::api::subsonic return _clientName; } - ProtocolVersion RequestContext::getServerProtocolVersion() const - { - return _serverProtocolVersion; - } - ResponseFormat RequestContext::getResponseFormat() const { return _responseFormat; diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index fe4c496c..233f56a8 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -60,7 +60,6 @@ namespace lms::api::subsonic std::string getClientIpAddr() const; std::string_view getClientName() const; - ProtocolVersion getServerProtocolVersion() const; ResponseFormat getResponseFormat() const; bool isOpenSubsonicEnabled() const; @@ -74,7 +73,6 @@ namespace lms::api::subsonic const ProtocolVersion _clientProtocolVersion; const ResponseFormat _responseFormat; - const ProtocolVersion _serverProtocolVersion; const bool _isOpenSubsonicEnabled; }; } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 825d6986..3c59b584 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -32,10 +32,9 @@ #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/User.hpp" -#include "services/auth/IAuthTokenService.hpp" #include "services/auth/IPasswordService.hpp" -#include "ParameterParsing.hpp" +#include "AuthUtils.hpp" #include "RequestContext.hpp" #include "SubsonicResponse.hpp" #include "endpoints/AlbumSongLists.hpp" @@ -269,15 +268,6 @@ namespace lms::api::subsonic TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete; }; - db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId) - { - auto transaction{ session.createReadTransaction() }; - - if (db::User::pointer user{ db::User::find(session, userId) }) - return user; - - throw UserNotAuthorizedError{}; - } } // namespace SubsonicResource::SubsonicResource(db::IDb& db) @@ -328,7 +318,7 @@ namespace lms::api::subsonic // Media retrieval endpoints are always authenticated but we don't reauth user for a continuation db::User::pointer user; if (!request.continuation()) - user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); + user = utils::getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); requestContext.setUser(user); @@ -383,7 +373,7 @@ namespace lms::api::subsonic } catch (const Error& e) { - writeResponse(Response::createFailedResponse(defaultServerProtocolVersion, e), ResponseFormat::xml); + writeResponse(Response::createFailedResponse(e), ResponseFormat::xml); throw; } @@ -396,7 +386,7 @@ namespace lms::api::subsonic db::User::pointer user; if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated) { - user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); + user = utils::getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes); requestContext->setUser(user); } @@ -417,7 +407,7 @@ namespace lms::api::subsonic } catch (const Error& e) { - Response resp{ Response::createFailedResponse(requestContext->getServerProtocolVersion(), e) }; + Response resp{ Response::createFailedResponse(e) }; writeResponse(resp, requestContext->getResponseFormat()); throw; } @@ -425,50 +415,6 @@ namespace lms::api::subsonic db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request) { - const auto& parameters{ request.getParameterMap() }; - - if (hasParameter(parameters, "t")) - throw ProvidedAuthenticationMechanismNotSupportedError{}; - - const auto user{ getParameterAs(parameters, "u") }; - const auto password{ getParameterAs(parameters, "p") }; - if (!_config.supportUserPasswordAuthentication && (password || user)) - throw ProvidedAuthenticationMechanismNotSupportedError{}; - - const auto apiKey{ getParameterAs(parameters, "apiKey") }; - - if (user && !password) - throw RequiredParameterMissingError{ "p" }; - if (!user && password) - throw RequiredParameterMissingError{ "u" }; - if (apiKey && password) - throw MultipleConflictingAuthenticationMechanismsProvidedError{}; - if (!apiKey && !password) - throw RequiredParameterMissingError{ "apiKey" }; - - const auto clientAddress{ boost::asio::ip::make_address(request.clientAddress()) }; - const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) }; - - const auto authResult{ core::Service::get()->processAuthToken("subsonic", clientAddress, authToken) }; - switch (authResult.state) - { - case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted: - if (user) - { - const auto authenticatedUser{ getUserFromUserId(_db.getTLSSession(), authResult.authTokenInfo->userId) }; - if (!authenticatedUser || authenticatedUser->getLoginName() != *user) - throw WrongUsernameOrPasswordError{}; - } - return authResult.authTokenInfo->userId; - case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: - if (apiKey) - throw InvalidAPIkeyError{}; - else - throw WrongUsernameOrPasswordError{}; - case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: - throw LoginThrottledGenericError{}; - } - - throw InternalErrorGenericError{ "Cannot authenticate user" }; + return utils::authenticateUser(request, _db.getTLSSession()); } } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResourceConfig.cpp b/src/libs/subsonic/impl/SubsonicResourceConfig.cpp index f33d50b5..c1ea001d 100644 --- a/src/libs/subsonic/impl/SubsonicResourceConfig.cpp +++ b/src/libs/subsonic/impl/SubsonicResourceConfig.cpp @@ -25,19 +25,6 @@ namespace lms::api::subsonic { namespace { - std::unordered_map readConfigProtocolVersions(core::IConfig& config) - { - std::unordered_map res; - - config.visitStrings("api-subsonic-old-server-protocol-clients", - [&](std::string_view client) { - res.emplace(std::string{ client }, ProtocolVersion{ .major = 1, .minor = 12, .patch = 0 }); - }, - { "DSub" }); - - return res; - } - std::unordered_set readOpenSubsonicDisabledClients(core::IConfig& config) { std::unordered_set res; @@ -55,9 +42,7 @@ namespace lms::api::subsonic SubsonicResourceConfig readSubsonicResourceConfig(core::IConfig& config) { return SubsonicResourceConfig{ - .serverProtocolVersionsByClient = readConfigProtocolVersions(config), .openSubsonicDisabledClients = readOpenSubsonicDisabledClients(config), - .supportUserPasswordAuthentication = config.getBool("api-subsonic-support-user-password-auth", true) }; } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/SubsonicResourceConfig.hpp b/src/libs/subsonic/impl/SubsonicResourceConfig.hpp index 0eef5698..4fbf73b0 100644 --- a/src/libs/subsonic/impl/SubsonicResourceConfig.hpp +++ b/src/libs/subsonic/impl/SubsonicResourceConfig.hpp @@ -20,11 +20,8 @@ #pragma once #include -#include #include -#include "ProtocolVersion.hpp" - namespace lms::core { class IConfig; @@ -34,9 +31,7 @@ namespace lms::api::subsonic { struct SubsonicResourceConfig { - std::unordered_map serverProtocolVersionsByClient; std::unordered_set openSubsonicDisabledClients; - bool supportUserPasswordAuthentication; }; SubsonicResourceConfig readSubsonicResourceConfig(core::IConfig& _config); diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index cd05c007..f0c96c1f 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -302,23 +302,23 @@ namespace lms::api::subsonic os << '\"'; } - Response Response::createOkResponse(ProtocolVersion protocolVersion) + Response Response::createOkResponse() { - return createResponseCommon(protocolVersion); + return createResponseCommon(); } - Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) + Response Response::createFailedResponse(const Error& error) { - return createResponseCommon(protocolVersion, &error); + return createResponseCommon(&error); } - Response Response::createResponseCommon(ProtocolVersion protocolVersion, const Error* error) + Response Response::createResponseCommon(const Error* error) { Response response; Node& responseNode{ response._root.createChild("subsonic-response") }; responseNode.setAttribute("status", error ? "failed" : "ok"); - responseNode.setVersionAttribute(protocolVersion); + responseNode.setVersionAttribute(serverProtocolVersion); if (error) { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 94d76898..128ce687 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -47,7 +47,6 @@ namespace lms::api::subsonic ServerMustUpgrade = 30, WrongUsernameOrPassword = 40, TokenAuthenticationNotSupportedForLDAPUsers = 41, - ProvidedAuthenticationMechanismNotSupported = 42, MultipleConflictingAuthenticationMechanismsProvided = 43, InvalidAPIkey = 44, UserNotAuthorized = 50, @@ -131,19 +130,6 @@ namespace lms::api::subsonic std::string getMessage() const override { return "Token authentication not supported for LDAP users."; } }; - class ProvidedAuthenticationMechanismNotSupportedError : public Error - { - public: - ProvidedAuthenticationMechanismNotSupportedError() - : Error{ Code::ProvidedAuthenticationMechanismNotSupported } {} - - private: - std::string getMessage() const override - { - return "Provided authentication mechanism not supported."; - } - }; - class MultipleConflictingAuthenticationMechanismsProvidedError : public Error { public: @@ -312,8 +298,8 @@ namespace lms::api::subsonic map _childrenValues; }; - static Response createOkResponse(ProtocolVersion protocolVersion); - static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error); + static Response createOkResponse(); + static Response createFailedResponse(const Error& error); ~Response() = default; Response(const Response&) = delete; @@ -328,7 +314,7 @@ namespace lms::api::subsonic void write(std::ostream& os, ResponseFormat format) const; private: - static Response createResponseCommon(ProtocolVersion protocolVersion, const Error* error = nullptr); + static Response createResponseCommon(const Error* error = nullptr); class JsonSerializer { diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index f5e8ff0f..c814511d 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -58,7 +58,7 @@ namespace lms::api::subsonic const Range range{ offset, size }; - RangeResults releases; + std::vector releases; scrobbling::IScrobblingService& scrobblingService{ *core::Service::get() }; feedback::IFeedbackService& feedbackService{ *core::Service::get() }; @@ -163,10 +163,10 @@ namespace lms::api::subsonic throw NotImplementedGenericError{}; } - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) }; - for (const ReleaseId releaseId : releases.results) + for (const ReleaseId releaseId : releases) { const Release::pointer release{ Release::find(context.getDbSession(), releaseId) }; albumListNode.addArrayChild("album", createAlbumNode(context, release, id3)); @@ -182,7 +182,7 @@ namespace lms::api::subsonic auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) }; feedback::IFeedbackService& feedbackService{ *core::Service::get() }; @@ -193,7 +193,7 @@ namespace lms::api::subsonic feedback::IFeedbackService::ArtistFindParameters artistFindParams; artistFindParams.setUser(context.getUser()->getId()); artistFindParams.setSortMethod(ArtistSortMethod::SortName); - for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results) + for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams)) { if (auto artist{ Artist::find(context.getDbSession(), artistId) }) starredNode.addArrayChild("artist", createArtistNode(context, artist)); @@ -204,13 +204,13 @@ namespace lms::api::subsonic findParameters.setUser(context.getUser()->getId()); findParameters.filters.setMediaLibrary(mediaLibrary); - for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results) + for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters)) { if (auto release{ Release::find(context.getDbSession(), releaseId) }) starredNode.addArrayChild("album", createAlbumNode(context, release, id3)); } - for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results) + for (const TrackId trackId : feedbackService.findStarredTracks(findParameters)) { if (auto track{ Track::find(context.getDbSession(), trackId) }) starredNode.addArrayChild("song", createSongNode(context, track, context.getUser())); @@ -238,7 +238,7 @@ namespace lms::api::subsonic if (size > defaultMaxCountSize) throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& randomSongsNode{ response.createNode("randomSongs") }; auto transaction{ context.getDbSession().createReadTransaction() }; @@ -274,7 +274,7 @@ namespace lms::api::subsonic if (!genreObj) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& songsByGenreNode{ response.createNode("songsByGenre") }; Track::FindParameters params; @@ -291,7 +291,7 @@ namespace lms::api::subsonic Response handleGetNowPlayingRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& nowPlayingNode{ response.createNode("nowPlaying") }; scrobbling::IScrobblingService& scrobblingService{ *core::Service::get() }; diff --git a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp index 3f9758ec..82c50465 100644 --- a/src/libs/subsonic/impl/endpoints/Bookmarks.cpp +++ b/src/libs/subsonic/impl/endpoints/Bookmarks.cpp @@ -38,10 +38,10 @@ namespace lms::api::subsonic const auto bookmarkIds{ db::TrackBookmark::find(context.getDbSession(), context.getUser()->getId()) }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& bookmarksNode{ response.createNode("bookmarks") }; - for (const db::TrackBookmarkId bookmarkId : bookmarkIds.results) + for (const db::TrackBookmarkId bookmarkId : bookmarkIds) { const db::TrackBookmark::pointer bookmark{ db::TrackBookmark::find(context.getDbSession(), bookmarkId) }; Response::Node bookmarkNode{ createBookmarkNode(bookmark) }; @@ -74,7 +74,7 @@ namespace lms::api::subsonic if (comment) bookmark.modify()->setComment(*comment); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleDeleteBookmark(RequestContext& context) @@ -90,7 +90,7 @@ namespace lms::api::subsonic bookmark.remove(); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } static db::PlayQueue::pointer getOrCreatePlayQueue(RequestContext& context) @@ -122,7 +122,7 @@ namespace lms::api::subsonic // PlayQueue makes use of a dedicated internal playlist static Response handleGetPlayQueueCommon(RequestContext& context, bool byIndex) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; const db::PlayQueue::pointer playQueue{ getOrCreatePlayQueue(context) }; assert(playQueue); @@ -204,7 +204,7 @@ namespace lms::api::subsonic playQueue.modify()->setLastModifiedDateTime(Wt::WDateTime::currentDateTime()); } - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleSavePlayQueue(RequestContext& context) diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index 2ebcb9a8..504f362a 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -69,7 +69,7 @@ namespace lms::api::subsonic } else { - res = Directory::findRootDirectories(session).results; + res = Directory::findRootDirectories(session); } return res; @@ -137,8 +137,8 @@ namespace lms::api::subsonic const auto artistTracks{ Track::findIds(context.getDbSession(), params) }; tracks.insert(std::end(tracks), - std::begin(artistTracks.results), - std::end(artistTracks.results)); + std::begin(artistTracks), + std::end(artistTracks)); } return tracks; @@ -173,8 +173,8 @@ namespace lms::api::subsonic const auto releaseTracks{ Track::findIds(context.getDbSession(), params) }; tracks.insert(std::end(tracks), - std::begin(releaseTracks.results), - std::end(releaseTracks.results)); + std::begin(releaseTracks), + std::end(releaseTracks)); } return tracks; @@ -214,7 +214,7 @@ namespace lms::api::subsonic auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& similarSongsNode{ response.createNode(id3 ? Response::Node::Key{ "similarSongs2" } : Response::Node::Key{ "similarSongs" }) }; for (const TrackId trackId : tracks) { @@ -244,7 +244,7 @@ namespace lms::api::subsonic Response handleGetMusicFoldersRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& musicFoldersNode{ response.createNode("musicFolders") }; auto transaction{ context.getDbSession().createReadTransaction() }; @@ -263,7 +263,7 @@ namespace lms::api::subsonic // Optional params const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& indexesNode{ response.createNode("indexes") }; indexesNode.setAttribute("ignoredArticles", ""); indexesNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write? @@ -310,7 +310,7 @@ namespace lms::api::subsonic // Mandatory params const auto directoryId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& directoryNode{ response.createNode("directory") }; auto transaction{ context.getDbSession().createReadTransaction() }; @@ -374,7 +374,7 @@ namespace lms::api::subsonic Response handleGetGenresRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& genresNode{ response.createNode("genres") }; @@ -392,7 +392,7 @@ namespace lms::api::subsonic // Optional params const MediaLibraryId mediaLibrary{ getParameterAs(context.getParameters(), "musicFolderId").value_or(MediaLibraryId{}) }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& artistsNode{ response.createNode("artists") }; artistsNode.setAttribute("ignoredArticles", ""); @@ -431,7 +431,7 @@ namespace lms::api::subsonic parameters.setRange(Range{ currentArtistOffset, batchSize }); const auto artists{ Artist::find(context.getDbSession(), parameters) }; - for (const Artist::pointer& artist : artists.results) + for (const Artist::pointer& artist : artists) { std::string_view sortName{ artist->getSortName() }; @@ -439,8 +439,8 @@ namespace lms::api::subsonic artistsSortedByFirstChar[sortChar].push_back(artist->getId()); } - hasMoreArtists = artists.moreResults; - currentArtistOffset += artists.results.size(); + hasMoreArtists = (artists.size() == batchSize); + currentArtistOffset += artists.size(); } // second pass: add each artist @@ -473,7 +473,7 @@ namespace lms::api::subsonic if (!artist) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node artistNode{ createArtistNode(context, artist) }; auto addRelease{ [&](const Release::pointer& release) { @@ -505,11 +505,11 @@ namespace lms::api::subsonic if (!release) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node albumNode{ createAlbumNode(context, release, true /* id3 */) }; const auto tracks{ Track::find(context.getDbSession(), Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) }; - for (const Track::pointer& track : tracks.results) + for (const Track::pointer& track : tracks) albumNode.addArrayChild("song", createSongNode(context, track, true /* id3 */)); response.addNode("album", std::move(albumNode)); @@ -528,7 +528,7 @@ namespace lms::api::subsonic if (!track) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("song", createSongNode(context, track, context.getUser())); return response; @@ -542,7 +542,7 @@ namespace lms::api::subsonic // Optional params std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(20) }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& artistInfoNode{ response.createNode(Response::Node::Key{ "artistInfo2" }) }; { @@ -601,7 +601,7 @@ namespace lms::api::subsonic { const db::DirectoryId directoryId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; { auto transaction{ context.getDbSession().createReadTransaction() }; @@ -616,7 +616,7 @@ namespace lms::api::subsonic { const db::ReleaseId releaseId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; { auto transaction{ context.getDbSession().createReadTransaction() }; @@ -648,7 +648,7 @@ namespace lms::api::subsonic auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& topSongs{ response.createNode("topSongs") }; const auto artists{ Artist::find(context.getDbSession(), artistName) }; @@ -660,7 +660,7 @@ namespace lms::api::subsonic params.setArtist(artists.front()->getId()); const auto trackIds{ core::Service::get()->getTopTracks(params) }; - for (const TrackId trackId : trackIds.results) + for (const TrackId trackId : trackIds) { if (Track::pointer track{ Track::find(context.getDbSession(), trackId) }) topSongs.addArrayChild("song", createSongNode(context, track, context.getUser())); @@ -684,7 +684,7 @@ namespace lms::api::subsonic auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; for (const auto& similarTrack : similarTracks) { @@ -715,7 +715,7 @@ namespace lms::api::subsonic auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; for (const auto& pathTrack : pathTracks) { diff --git a/src/libs/subsonic/impl/endpoints/Jukebox.cpp b/src/libs/subsonic/impl/endpoints/Jukebox.cpp index 410de2cc..582abee6 100644 --- a/src/libs/subsonic/impl/endpoints/Jukebox.cpp +++ b/src/libs/subsonic/impl/endpoints/Jukebox.cpp @@ -71,7 +71,7 @@ namespace lms::api::subsonic Response handleJukeboxGet(RequestContext& context, jukebox::IJukeboxService& jukeboxService) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node jukeboxPlaylistNode{ createJukeboxStatusNode(jukeboxService) }; { @@ -88,9 +88,9 @@ namespace lms::api::subsonic return response; } - Response handleJukeboxStatus(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + Response handleJukeboxStatus(RequestContext& /*context*/, jukebox::IJukeboxService& jukeboxService) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } @@ -103,25 +103,25 @@ namespace lms::api::subsonic jukeboxService.clearTracks(); jukeboxService.appendTracks(trackIds); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } - Response handleJukeboxStart(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + Response handleJukeboxStart(RequestContext& /*context*/, jukebox::IJukeboxService& jukeboxService) { jukeboxService.resume(); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } - Response handleJukeboxStop(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + Response handleJukeboxStop(RequestContext& /*context*/, jukebox::IJukeboxService& jukeboxService) { jukeboxService.pause(); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } @@ -134,7 +134,7 @@ namespace lms::api::subsonic // do not report potential range error jukeboxService.play(index, std::chrono::seconds{ offset }); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } @@ -145,16 +145,16 @@ namespace lms::api::subsonic jukeboxService.appendTracks(trackIds); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } - Response handleJukeboxClear(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + Response handleJukeboxClear(RequestContext& /*context*/, jukebox::IJukeboxService& jukeboxService) { jukeboxService.clearTracks(); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } @@ -164,16 +164,16 @@ namespace lms::api::subsonic const auto index{ getMandatoryParameterAs(context.getParameters(), "index") }; jukeboxService.removeTrack(index); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } - Response handleJukeboxShuffle(RequestContext& context, jukebox::IJukeboxService& jukeboxService) + Response handleJukeboxShuffle(RequestContext& /*context*/, jukebox::IJukeboxService& jukeboxService) { jukeboxService.shuffleTracks(); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } @@ -186,7 +186,7 @@ namespace lms::api::subsonic jukeboxService.setVolume(gain); // consider gain is linear - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("jukeboxStatus", createJukeboxStatusNode(jukeboxService)); return response; } diff --git a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp index 6df094dc..35cb6431 100644 --- a/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaAnnotation.cpp @@ -130,7 +130,7 @@ namespace lms::api::subsonic for (const TrackId id : params.trackIds) core::Service::get()->star(context.getUser()->getId(), id); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleUnstarRequest(RequestContext& context) @@ -152,7 +152,7 @@ namespace lms::api::subsonic for (const TrackId id : params.trackIds) core::Service::get()->unstar(context.getUser()->getId(), id); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleSetRating(RequestContext& context) @@ -171,7 +171,7 @@ namespace lms::api::subsonic else if (const TrackId * trackId{ std::get_if(¶ms.id) }) core::Service::get()->setRating(context.getUser()->getId(), *trackId, params.rating); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleScrobble(RequestContext& context) @@ -209,6 +209,6 @@ namespace lms::api::subsonic } } - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp b/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp index 2c09c161..25d5dfaf 100644 --- a/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaLibraryScanning.cpp @@ -49,19 +49,19 @@ namespace lms::api::subsonic::Scan } } // namespace - Response handleGetScanStatus(RequestContext& context) + Response handleGetScanStatus(RequestContext& /*context*/) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("scanStatus", createStatusResponseNode()); return response; } - Response handleStartScan(RequestContext& context) + Response handleStartScan(RequestContext& /*context*/) { core::Service::get()->requestImmediateScan(); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("scanStatus", createStatusResponseNode()); return response; diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 47446d26..e6a0bb1d 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -236,7 +236,7 @@ namespace lms::api::subsonic std::string artistName{ getParameterAs(context.getParameters(), "artist").value_or("") }; std::string titleName{ getParameterAs(context.getParameters(), "title").value_or("") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; // best effort search, as this API is really limited auto transaction{ context.getDbSession().createReadTransaction() }; @@ -248,11 +248,11 @@ namespace lms::api::subsonic // Choice: we return nothing if there are too many results const auto tracks{ db::Track::findIds(context.getDbSession(), params) }; - if (tracks.results.size() == 1) + if (tracks.size() == 1) { // Choice: we return only the first lyrics if the track has many lyrics db::TrackLyrics::FindParameters lyricsParams; - lyricsParams.setTrack(tracks.results[0]); + lyricsParams.setTrack(tracks[0]); lyricsParams.setSortMethod(db::TrackLyricsSortMethod::ExternalFirst); lyricsParams.setRange(db::Range{ 0, 1 }); @@ -269,7 +269,7 @@ namespace lms::api::subsonic // mandatory params db::TrackId id{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& lyricsList{ response.createNode("lyricsList") }; lyricsList.createEmptyArrayChild("structuredLyrics"); diff --git a/src/libs/subsonic/impl/endpoints/Playlists.cpp b/src/libs/subsonic/impl/endpoints/Playlists.cpp index 00ae150e..3973a562 100644 --- a/src/libs/subsonic/impl/endpoints/Playlists.cpp +++ b/src/libs/subsonic/impl/endpoints/Playlists.cpp @@ -55,7 +55,7 @@ namespace lms::api::subsonic { auto transaction{ context.getDbSession().createReadTransaction() }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& playlistsNode{ response.createNode("playlists") }; auto addTrackList{ [&](const db::TrackList::pointer& trackList) { @@ -103,11 +103,11 @@ namespace lms::api::subsonic if (trackList->getUserId() != context.getUser()->getId() && trackList->getVisibility() != TrackList::Visibility::Public) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; - for (const TrackListEntry::pointer& entry : entries.results) + for (const TrackListEntry::pointer& entry : entries) playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.getUser())); response.addNode("playlist", std::move(playlistNode)); @@ -156,11 +156,11 @@ namespace lms::api::subsonic context.getDbSession().create(track, trackList); } - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node playlistNode{ createPlaylistNode(context, trackList) }; auto entries{ trackList->getEntries() }; - for (const TrackListEntry::pointer& entry : entries.results) + for (const TrackListEntry::pointer& entry : entries) playlistNode.addArrayChild("entry", createSongNode(context, entry->getTrack(), context.getUser())); response.addNode("playlist", std::move(playlistNode)); @@ -213,7 +213,7 @@ namespace lms::api::subsonic context.getDbSession().create(track, trackList); } - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleDeletePlaylistRequest(RequestContext& context) @@ -227,6 +227,6 @@ namespace lms::api::subsonic trackList.remove(); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/endpoints/Podcast.cpp b/src/libs/subsonic/impl/endpoints/Podcast.cpp index 389462c1..13ebd833 100644 --- a/src/libs/subsonic/impl/endpoints/Podcast.cpp +++ b/src/libs/subsonic/impl/endpoints/Podcast.cpp @@ -37,7 +37,7 @@ namespace lms::api::subsonic const bool includeEpisodes{ getParameterAs(context.getParameters(), "includeEpisodes").value_or(true) }; const std::optional podcastId{ getParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& podcastsNode{ response.createNode("podcasts") }; podcastsNode.createEmptyArrayChild("channel"); @@ -66,7 +66,7 @@ namespace lms::api::subsonic std::size_t count{ getParameterAs(context.getParameters(), "count").value_or(20) }; count = std::min(count, 100); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& newestPodcastsNode{ response.createNode("newestPodcasts") }; newestPodcastsNode.createEmptyArrayChild("episode"); @@ -84,11 +84,11 @@ namespace lms::api::subsonic return response; } - Response handleRefreshPodcasts(RequestContext& context) + Response handleRefreshPodcasts(RequestContext& /*context*/) { core::Service::get()->refreshPodcasts(); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleCreatePodcastChannel(RequestContext& context) @@ -102,7 +102,7 @@ namespace lms::api::subsonic // no effect if podcast already exists core::Service::get()->addPodcast(url); - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleDeletePodcastChannel(RequestContext& context) @@ -113,7 +113,7 @@ namespace lms::api::subsonic if (!core::Service::get()->removePodcast(podcastId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleDeletePodcastEpisode(RequestContext& context) @@ -124,7 +124,7 @@ namespace lms::api::subsonic if (!core::Service::get()->deletePodcastEpisode(episodeId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleDownloadPodcastEpisode(RequestContext& context) @@ -135,7 +135,7 @@ namespace lms::api::subsonic if (!core::Service::get()->downloadPodcastEpisode(episodeId)) throw RequestedDataNotFoundError{}; - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } Response handleGetPodcastEpisode(RequestContext& context) @@ -143,7 +143,7 @@ namespace lms::api::subsonic // Mandatory parameters const db::PodcastEpisodeId episodeId{ getMandatoryParameterAs(context.getParameters(), "id") }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; auto transaction{ context.getDbSession().createReadTransaction() }; diff --git a/src/libs/subsonic/impl/endpoints/Searching.cpp b/src/libs/subsonic/impl/endpoints/Searching.cpp index e6ab740c..3f178d9e 100644 --- a/src/libs/subsonic/impl/endpoints/Searching.cpp +++ b/src/libs/subsonic/impl/endpoints/Searching.cpp @@ -373,7 +373,7 @@ namespace lms::api::subsonic if (!query.empty()) keywords = core::stringUtils::splitString(query, ' '); - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& searchResultNode{ response.createNode(id3 ? "searchResult3" : "searchResult2") }; auto transaction{ context.getDbSession().createReadTransaction() }; diff --git a/src/libs/subsonic/impl/endpoints/System.cpp b/src/libs/subsonic/impl/endpoints/System.cpp index 5263dc17..32ff7b3e 100644 --- a/src/libs/subsonic/impl/endpoints/System.cpp +++ b/src/libs/subsonic/impl/endpoints/System.cpp @@ -26,14 +26,14 @@ namespace lms::api::subsonic { - Response handlePingRequest(RequestContext& context) + Response handlePingRequest(RequestContext& /*context*/) { - return Response::createOkResponse(context.getServerProtocolVersion()); + return Response::createOkResponse(); } - Response handleGetLicenseRequest(RequestContext& context) + Response handleGetLicenseRequest(RequestContext& /*context*/) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& licenseNode{ response.createNode("license") }; licenseNode.setAttribute("licenseExpires", "2035-09-03T14:46:43"); @@ -45,7 +45,7 @@ namespace lms::api::subsonic Response handleTokenInfoRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& tokenInfoNode{ response.createNode("tokenInfo") }; auto transaction{ context.getDbSession().createReadTransaction() }; @@ -54,9 +54,9 @@ namespace lms::api::subsonic return response; } - Response handleGetOpenSubsonicExtensions(RequestContext& context) + Response handleGetOpenSubsonicExtensions(RequestContext& /*context*/) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; struct Extension { diff --git a/src/libs/subsonic/impl/endpoints/Transcoding.cpp b/src/libs/subsonic/impl/endpoints/Transcoding.cpp index 2ef4bc08..f8cc0014 100644 --- a/src/libs/subsonic/impl/endpoints/Transcoding.cpp +++ b/src/libs/subsonic/impl/endpoints/Transcoding.cpp @@ -84,7 +84,7 @@ namespace lms::api::subsonic const ClientInfo clientInfo{ parseClientInfoFromJson(context.getBody()) }; const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioFileId) }; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& transcodeNode{ response.createNode("transcodeDecision") }; { diff --git a/src/libs/subsonic/impl/endpoints/UserManagement.cpp b/src/libs/subsonic/impl/endpoints/UserManagement.cpp index 02b32e75..3812422d 100644 --- a/src/libs/subsonic/impl/endpoints/UserManagement.cpp +++ b/src/libs/subsonic/impl/endpoints/UserManagement.cpp @@ -50,7 +50,7 @@ namespace lms::api::subsonic if (!user) throw RequestedDataNotFoundError{}; - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; response.addNode("user", createUserNode(context, user)); return response; @@ -58,7 +58,7 @@ namespace lms::api::subsonic Response handleGetUsersRequest(RequestContext& context) { - Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response response{ Response::createOkResponse() }; Response::Node& usersNode{ response.createNode("users") }; auto transaction{ context.getDbSession().createReadTransaction() }; diff --git a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp index a5b877bb..6a94e610 100644 --- a/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp +++ b/src/libs/subsonic/impl/endpoints/transcoding/TranscodeDecision.cpp @@ -308,6 +308,7 @@ namespace lms::api::subsonic::detail throw InternalErrorGenericError{ "Unhandled limitation comparison operator" }; } + // A non-required limitation is a preference only: it must never force a transcode away from direct play bool isStreamCompatibleWithLimitation(const audio::AudioProperties& source, const Limitation& limitation) { if (!limitation.required) @@ -397,6 +398,9 @@ namespace lms::api::subsonic::detail return std::nullopt; } + // Unlike isStreamCompatibleWithLimitation, this ignores `required` entirely: + // once transcoding is already happening for any reason, + // every limitation of the matched target codec profile still shapes the output AdjustResult applyLimitation(const audio::AudioProperties& source, const Limitation& limitation, StreamDetails& transcodedStream) { switch (limitation.name) @@ -512,6 +516,10 @@ namespace lms::api::subsonic::detail } } + // Lossy codecs have no meaningful PCM bit depth: don't report the source's bit depth as if it still applied + if (!core::media::getCodecDesc(transcodeFormat->codec).isLossless) + transcodedStream.audioBitdepth.reset(); + return transcodedStream; } diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index c76b8f7b..70dd7056 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -134,7 +134,7 @@ namespace lms::api::subsonic Genre::FindParameters genreParams; genreParams.setRelease(release->getId()); genreParams.setSortMethod(GenreSortMethod::TrackCountDesc); - const auto genres{ Genre::find(context.getDbSession(), genreParams).results }; + const auto genres{ Genre::find(context.getDbSession(), genreParams) }; if (!genres.empty()) albumNode.setAttribute("genre", genres.front()->getName()); diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 5289a7fc..b0192118 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -36,11 +36,13 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Mood.hpp" +#include "database/objects/Movement.hpp" #include "database/objects/Release.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" +#include "database/objects/Work.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -250,6 +252,28 @@ namespace lms::api::subsonic trackResponse.addChild("replayGain", createReplayGainNode(track, medium)); + trackResponse.createEmptyArrayChild("works"); + for (const auto& work : track->getWorks()) + { + Response::Node workNode; + workNode.setAttribute("name", work->getName()); + if (const auto mbid{ work->getMBID() }) + workNode.setAttribute("musicBrainzId", mbid->toString()); + trackResponse.addArrayChild("works", std::move(workNode)); + } + + trackResponse.createEmptyArrayChild("movements"); + for (const auto& movement : track->getMovements()) + { + Response::Node movementNode; + movementNode.setAttribute("name", movement->getName()); + if (const auto n{ movement->getNumber() }) + movementNode.setAttribute("number", *n); + if (const auto c{ movement->getCount() }) + movementNode.setAttribute("count", *c); + trackResponse.addArrayChild("movements", std::move(movementNode)); + } + return trackResponse; } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/test/AuthUtils.cpp b/src/libs/subsonic/test/AuthUtils.cpp new file mode 100644 index 00000000..e8eb3174 --- /dev/null +++ b/src/libs/subsonic/test/AuthUtils.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2026 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 + +#include "AuthUtils.hpp" +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic::utils::tests +{ + // Reference values from the Subsonic API documentation (apiKey = "sesame", salt = "c19b2d") + TEST(AuthUtils, checkAuthToken_ValidToken) + { + EXPECT_TRUE(checkAuthToken("sesame", "c19b2d", "26719a1196d2a940705a59634eb18eab")); + } + + TEST(AuthUtils, checkAuthToken_ValidToken_UppercaseHex) + { + EXPECT_TRUE(checkAuthToken("sesame", "c19b2d", "26719A1196D2A940705A59634EB18EAB")); + } + + TEST(AuthUtils, checkAuthToken_WrongToken) + { + EXPECT_FALSE(checkAuthToken("sesame", "c19b2d", "00000000000000000000000000000000")); + } + + TEST(AuthUtils, checkAuthToken_WrongSalt) + { + EXPECT_FALSE(checkAuthToken("sesame", "differentsalt", "26719a1196d2a940705a59634eb18eab")); + } + + TEST(AuthUtils, checkAuthToken_WrongApiKey) + { + EXPECT_FALSE(checkAuthToken("wrongkey", "c19b2d", "26719a1196d2a940705a59634eb18eab")); + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyOnly) + { + const auto request{ parseAndValidateAuthenticationRequest({ { "apiKey", { "apiKey" } } }) }; + ASSERT_TRUE(std::holds_alternative(request)); + EXPECT_EQ(std::get(request).apiKey, "apiKey"); + + EXPECT_THROW(parseAndValidateAuthenticationRequest({}), RequiredParameterMissingError); + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_password) + { + const auto request{ parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } } }) }; + ASSERT_TRUE(std::holds_alternative(request)); + EXPECT_EQ(std::get(request).user, "user"); + EXPECT_EQ(std::get(request).password, "password"); + + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "p", { "password" } } }), RequiredParameterMissingError); // missing u + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } } }), RequiredParameterMissingError); // missing p + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_token) + { + const auto request{ parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } }, { "s", { "salt" } } }) }; + ASSERT_TRUE(std::holds_alternative(request)); + EXPECT_EQ(std::get(request).user, "user"); + EXPECT_EQ(std::get(request).token, "token"); + EXPECT_EQ(std::get(request).salt, "salt"); + + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "t", { "token" } }, { "s", { "salt" } } }), RequiredParameterMissingError); // missing u + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "s", { "salt" } } }), RequiredParameterMissingError); // missing t + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } } }), RequiredParameterMissingError); // missing s + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_conflicts) + { + // password + token + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } }, { "t", { "token" } }, { "s", { "salt" } } }), MultipleConflictingAuthenticationMechanismsProvidedError); + // apiKey + password + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } }, { "apiKey", { "apiKey" } } }), MultipleConflictingAuthenticationMechanismsProvidedError); + // apiKey + token + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } }, { "s", { "salt" } }, { "apiKey", { "apiKey" } } }), MultipleConflictingAuthenticationMechanismsProvidedError); + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_usernameOnlyRequiresPassword) + { + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } } }), RequiredParameterMissingError); // missing p + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyWithPartialPassword) + { + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "p", { "password" } }, { "apiKey", { "apiKey" } } }), RequiredParameterMissingError); // missing u + } + + TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyWithPartialToken) + { + EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "s", { "salt" } }, { "apiKey", { "apiKey" } } }), RequiredParameterMissingError); // missing u + } +} // namespace lms::api::subsonic::utils::tests diff --git a/src/libs/subsonic/test/CMakeLists.txt b/src/libs/subsonic/test/CMakeLists.txt index 39678b48..a97c7d39 100644 --- a/src/libs/subsonic/test/CMakeLists.txt +++ b/src/libs/subsonic/test/CMakeLists.txt @@ -1,6 +1,7 @@ include(GoogleTest) add_executable(test-subsonic + AuthUtils.cpp ClientInfo.cpp Subsonic.cpp SubsonicResponse.cpp @@ -14,6 +15,7 @@ target_include_directories(test-subsonic PRIVATE target_link_libraries(test-subsonic PRIVATE lmscore lmsaudio + lmsdatabase lmssubsonic GTest::GTest ) diff --git a/src/libs/subsonic/test/SubsonicResponse.cpp b/src/libs/subsonic/test/SubsonicResponse.cpp index 1d3f8cb7..a384e24e 100644 --- a/src/libs/subsonic/test/SubsonicResponse.cpp +++ b/src/libs/subsonic/test/SubsonicResponse.cpp @@ -25,7 +25,6 @@ #include "core/String.hpp" #include "core/Version.hpp" -#include "ProtocolVersion.hpp" #include "SubsonicResponse.hpp" namespace lms::api::subsonic::tests @@ -34,7 +33,7 @@ namespace lms::api::subsonic::tests { Response generateFakeResponse() { - Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + Response response{ Response::createOkResponse() }; Response::Node& node{ response.createNode("MyNode") }; node.setAttribute("Attr1", "value1"); @@ -62,7 +61,7 @@ namespace lms::api::subsonic::tests TEST(SubsonicResponse, emptyJson) { - Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + Response response{ Response::createOkResponse() }; std::ostringstream oss; response.write(oss, ResponseFormat::json); @@ -88,7 +87,7 @@ namespace lms::api::subsonic::tests TEST(SubsonicResponse, emptyXml) { - Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + Response response{ Response::createOkResponse() }; std::ostringstream oss; response.write(oss, ResponseFormat::xml); @@ -116,7 +115,7 @@ namespace lms::api::subsonic::tests TEST(SubsonicResponse, jsonNaNAndInfinity) { - Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + Response response{ Response::createOkResponse() }; Response::Node& node{ response.createNode("MyMath") }; node.setAttribute("finite", 1.25F); diff --git a/src/libs/subsonic/test/TranscodeDecision.cpp b/src/libs/subsonic/test/TranscodeDecision.cpp index c8fa5ed0..fd21fdb5 100644 --- a/src/libs/subsonic/test/TranscodeDecision.cpp +++ b/src/libs/subsonic/test/TranscodeDecision.cpp @@ -123,6 +123,117 @@ namespace lms::api::subsonic .expected = { detail::DirectPlayResult{} }, }, + // check protocol * and codec * are properly handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::DirectPlayResult{} }, + }, + + // check container * is properly handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::DirectPlayResult{} }, + }, + + // MP4 container with ALAC (lossless) codec and no codec restriction direct-plays like any other supported container/codec pair + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = std::nullopt, + .maxTranscodingAudioBitrate = std::nullopt, + .directPlayProfiles = { + { .containers = { "m4a", "mp4" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MP4, + .codec = core::media::Codec::ALAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 1'011'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = 16, + }, + + .expected = { detail::DirectPlayResult{} }, + }, + + // MP4 container with an explicit aac audioCodecs restriction (as declared by real AAC-only profiles) direct-plays when the codec matches + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = std::nullopt, + .maxTranscodingAudioBitrate = std::nullopt, + .directPlayProfiles = { + { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = {}, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MP4, + .codec = core::media::Codec::AAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 257'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::DirectPlayResult{} }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, bitrateLimitation) + { + const TestCase testCases[]{ // Needs transcode due to codec limitation { .clientInfo = { @@ -213,6 +324,39 @@ namespace lms::api::subsonic .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, + // want flac but bitrate too high + { + .clientInfo = { + .name = "LocalDevice", + .platform = "Android", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 }, + }, + .transcodingProfiles = { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 1'000'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, sampleRateLimitation) + { + const TestCase testCases[]{ // Needs transcode due to max audio sample rate not handle by codec limitation { .clientInfo = { @@ -243,214 +387,6 @@ namespace lms::api::subsonic .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, - // Needs transcode due to max nb channels not handle by profile - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 320'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 } }, - .transcodingProfiles = { { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, - .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, - }, - .source = { - .container = core::media::Container::MPEG, - .codec = core::media::Codec::MP3, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 192'000, - .channelCount = 5, - .sampleRate = 48'000, - .bitsPerSample = std::nullopt, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - - // Needs transcode due to max nb channels not handle by codec. TODO take channel reduction into account for bitrate - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 320'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, - }, - .transcodingProfiles = { - { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, - }, - .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, - }, - .source = { - .container = core::media::Container::MPEG, - .codec = core::media::Codec::MP3, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 192'000, - .channelCount = 5, - .sampleRate = 48'000, - .bitsPerSample = std::nullopt, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - - // needs transcode because codec not handled - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 320'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, - }, - .transcodingProfiles = { - { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, - }, - .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, - }, - .source = { - .container = core::media::Container::Ogg, - .codec = core::media::Codec::Opus, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 128'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = std::nullopt, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - - // needs transcode because codec not handled (lossless source => using max bitrate) - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 1'000'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, - }, - .transcodingProfiles = { - { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, - }, - .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, - }, - .source = { - .container = core::media::Container::FLAC, - .codec = core::media::Codec::FLAC, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 750'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = 16, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 320000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - - // needs transcode because codec not handled (lossless source => using a default good bitrate) - { - .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { - { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, - } }, - .transcodingProfiles = { - { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, - }, - .codecProfiles = {} }, - .source = { - .container = core::media::Container::FLAC, - .codec = core::media::Codec::FLAC, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 750'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = 16, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - - // check protocol * and codec * are properly handled - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 1'000'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, - }, - .transcodingProfiles = {}, - .codecProfiles = {}, - }, - .source = { - .container = core::media::Container::MPEG, - .codec = core::media::Codec::MP3, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 128'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = std::nullopt, - }, - - .expected = { detail::DirectPlayResult{} }, - }, - - // check container * is properly handled - { - .clientInfo = { - .name = "TestClient", - .platform = "TestPlatform", - .maxAudioBitrate = 1'000'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, - }, - .transcodingProfiles = {}, - .codecProfiles = {}, - }, - .source = { - .container = core::media::Container::MPEG, - .codec = core::media::Codec::MP3, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 128'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = std::nullopt, - }, - - .expected = { detail::DirectPlayResult{} }, - }, - - // want flac but bitrate too high - { - .clientInfo = { - .name = "LocalDevice", - .platform = "Android", - .maxAudioBitrate = 320'000, - .maxTranscodingAudioBitrate = 320'000, - .directPlayProfiles = { - { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 }, - }, - .transcodingProfiles = { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, - .codecProfiles = {}, - }, - .source = { - .container = core::media::Container::FLAC, - .codec = core::media::Codec::FLAC, - .duration = std::chrono::seconds{ 60 }, - .bitrate = 1'000'000, - .channelCount = 2, - .sampleRate = 48'000, - .bitsPerSample = 16, - }, - - .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 320'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, - }, - // want flac but source sample rate is too high { .clientInfo = { @@ -523,6 +459,206 @@ namespace lms::api::subsonic .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, channelLimitation) + { + const TestCase testCases[]{ + // Needs transcode due to max nb channels not handle by profile + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 } }, + .transcodingProfiles = { { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 5, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Needs transcode due to max nb channels not handle by codec. TODO take channel reduction into account for bitrate + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 192'000, + .channelCount = 5, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = 2, .audioBitrate = 192'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, codecFallbackSelection) + { + const TestCase testCases[]{ + // needs transcode because codec not handled + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "2" }, .required = true } } } }, + }, + .source = { + .container = core::media::Container::Ogg, + .codec = core::media::Codec::Opus, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because codec not handled (lossless source => using max bitrate) + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 320000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because codec not handled (lossless source => using a default good bitrate) + { + .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = {} }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // PCM (WAV) is a lossless codec: a WAV source must be able to fall back to a lossless transcoding target, just like FLAC + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = std::nullopt, + .maxTranscodingAudioBitrate = std::nullopt, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::WAV, + .codec = core::media::Codec::PCM, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 1'411'200, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // PCM (WAV) is lossless, so transcoding to a lossy target with no bitrate cap must pick a sane default bitrate, not the raw PCM bitrate + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = std::nullopt, + .maxTranscodingAudioBitrate = std::nullopt, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::WAV, + .codec = core::media::Codec::PCM, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 1'411'200, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, // wants a lossy codec not handled -> transcode to lossy { @@ -595,7 +731,14 @@ namespace lms::api::subsonic .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported, detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, }, + }; + processTests(testCases); + } + + TEST(TranscodeDecision, profileMatching) + { + const TestCase testCases[]{ // no protocol specified { .clientInfo = { @@ -625,6 +768,411 @@ namespace lms::api::subsonic .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitrateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 96000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, }, + + // needs transcode because audio codec not supported by the direct play profile (container matches, codec does not) + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "aac" }, .protocols = { "http" }, .maxAudioChannels = 2 }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioCodecNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // needs transcode because the direct play profile does not support the http protocol + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "hls" }, .maxAudioChannels = 2 }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ProtocolNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, bitDepth) + { + const TestCase testCases[]{ + // transcoding to a lossy codec must not report a bit depth, even if a bit depth limitation is set on that codec (lossy codecs have no PCM bit depth) + { + .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "16" }, .required = true }, + } } } }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 24, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "mp3", .codec = "mp3", .audioChannels = std::nullopt, .audioBitrate = 256000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // transcoding to a lossless codec still reports the adjusted bit depth + { + .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + } }, + .transcodingProfiles = { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = { + { .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "16" }, .required = true }, + } } } }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 24, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = 16 } } }, + }, + + // needs transcode because the codec profile bit depth limitation rejects direct play + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = { + { .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "16" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = 24, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitdepthNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = 16 } } }, + }, + + // a required bit depth limitation cannot be evaluated against a lossy source (no bit depth to check), so it is always treated as incompatible on both the direct play and transcoding target sides + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitdepth, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "24" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioBitdepthNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, optionalLimitation) + { + const TestCase testCases[]{ + // A non-required limitation is a preference only: it never blocks direct play, even when clearly violated (source has 6 channels vs the allowed 1 or 2) + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = { + { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "192000" }, .required = false }, + { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::Equals, .values = { "1", "2" }, .required = false }, + } } }, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 900'000, + .channelCount = 6, + .sampleRate = 96'000, + .bitsPerSample = 24, + }, + + .expected = { detail::DirectPlayResult{} }, + }, + + // A non-required limitation still shapes the transcoded target once a transcode is already happening for an unrelated reason (container mismatch): applyLimitation never checks `required` + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = { + { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "192000" }, .required = false }, + { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::Equals, .values = { "1", "2" }, .required = false }, + } } }, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 900'000, + .channelCount = 6, + .sampleRate = 96'000, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = 2, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, comparisonOperators) + { + const TestCase testCases[]{ + // Equals comparison (single value): mismatch cannot be adjusted -> rejects direct play, falls back to a transcoding profile not covered by that codec profile + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::Equals, .values = { "44100" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 48'000, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioSampleRateNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // Equals comparison (multiple values): no exact match -> adjusted down to the closest allowed value below the source's + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "flac", .limitations = { + { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::Equals, .values = { "44100", "48000", "96000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::FLAC, + .codec = core::media::Codec::FLAC, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 750'000, + .channelCount = 2, + .sampleRate = 60'000, + .bitsPerSample = 16, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "flac", .codec = "flac", .audioChannels = std::nullopt, .audioBitrate = std::nullopt, .audioProfile = "", .audioSamplerate = 48'000, .audioBitdepth = std::nullopt } } }, + }, + + // NotEquals comparison: source value is in the forbidden list and cannot be adjusted -> rejects direct play, falls back to a transcoding profile not covered by that codec profile + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioChannels, .comparison = Limitation::ComparisonOperator::NotEquals, .values = { "2" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::AudioChannelsNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + + // GreaterThanEqual comparison: source bitrate is below the required minimum and cannot be upscaled -> that transcoding profile is rejected entirely, falls back to the next one + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 1'000'000, + .maxTranscodingAudioBitrate = 1'000'000, + .directPlayProfiles = { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, + { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { + { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::GreaterThanEqual, .values = { "192000" }, .required = true }, + } } }, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::TranscodeResult{ .reasons = { detail::TranscodeReason::ContainerNotSupported }, .targetStreamInfo = { .protocol = "http", .container = "ogg", .codec = "opus", .audioChannels = std::nullopt, .audioBitrate = 128'000, .audioProfile = "", .audioSamplerate = std::nullopt, .audioBitdepth = std::nullopt } } }, + }, + }; + + processTests(testCases); + } + + TEST(TranscodeDecision, failure) + { + const TestCase testCases[]{ + // no compatible direct play or transcoding profile at all: transcoding profiles are skipped (unsupported output format, then non-http protocol) -> failure + { + .clientInfo = { + .name = "TestClient", + .platform = "TestPlatform", + .maxAudioBitrate = 320'000, + .maxTranscodingAudioBitrate = 320'000, + .directPlayProfiles = { + { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, + }, + .transcodingProfiles = { + { .container = "wma", .audioCodec = "wma", .protocol = "http", .maxAudioChannels = std::nullopt }, + { .container = "mp3", .audioCodec = "mp3", .protocol = "hls", .maxAudioChannels = std::nullopt }, + }, + .codecProfiles = {}, + }, + .source = { + .container = core::media::Container::MPEG, + .codec = core::media::Codec::MP3, + .duration = std::chrono::seconds{ 60 }, + .bitrate = 128'000, + .channelCount = 2, + .sampleRate = 44'100, + .bitsPerSample = std::nullopt, + }, + + .expected = { detail::FailureResult{ "No compatible direct play or transcoding profile found" } }, + }, }; processTests(testCases); diff --git a/src/lms/main.cpp b/src/lms/main.cpp index a409b759..82b7b04e 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -34,10 +34,13 @@ #include "core/Service.hpp" #include "core/String.hpp" #include "core/SystemPaths.hpp" +#include "core/UUID.hpp" #include "audio/IAudioOutput.hpp" +#include "audio/IMusicNNEmbeddingExtractor.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" +#include "database/objects/ServerInfo.hpp" #include "database/profiling/IQueryProfiler.hpp" #include "image/Image.hpp" #include "services/artwork/IArtworkService.hpp" @@ -110,6 +113,12 @@ namespace lms throw core::LmsException{ "Invalid config value for 'jukebox-audio-backend'" }; } + core::UUID getServerInstanceId(db::Session& session) + { + auto transaction{ session.createReadTransaction() }; + return db::ServerInfo::get(session)->getInstanceId(); + } + std::error_code checkDirectoryAccessible(const std::filesystem::path& dir) { std::error_code ec; @@ -432,12 +441,17 @@ namespace lms // Connection pool size must be twice the number of threads: we have at least 2 io pools with getThreadCount() each and they all may access the database auto database{ db::createDb(config->getPath("working-dir", "/var/lms") / "lms.db", getThreadCount() * 2) }; + core::UUID serverInstanceId; { db::Session session{ *database }; session.prepareTablesIfNeeded(); bool migrationPerformed{ session.migrateSchemaIfNeeded() }; + session.createScanSettingsIfNeeded(audio::canExtractMusicNNEmbeddings() ? db::RecommendationEngineType::AudioSimilarity : db::RecommendationEngineType::Clusters); + session.createServerInfoIfNeeded(); session.createIndexesIfNeeded(); + serverInstanceId = getServerInstanceId(session); + // As this may be quite long, we only do it during startup if (migrationPerformed) session.vacuum(); @@ -515,8 +529,8 @@ namespace lms // bind UI entry point server.addEntryPoint(Wt::EntryPointType::Application, - [&database, &appManager, uiAuthenticationBackend](const Wt::WEnvironment& env) { - return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend); + [&database, &appManager, uiAuthenticationBackend, serverInstanceId](const Wt::WEnvironment& env) { + return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend, serverInstanceId); }); proxyScannerEventsToApplication(*scannerService, server); diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index f8cfb114..9715241f 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -44,16 +44,26 @@ namespace lms::ui namespace { static constexpr core::LiteralString authTokenDomain{ "ui" }; - static const std::string authCookieName{ "LmsAuth" }; - static const std::string authCookieSalt{ Wt::Auth::SHA1HashFunction{}.compute(authCookieName, authTokenDomain.c_str()) }; // changing this will invalidate existing tokens + + // Scoped per-instance so several LMS instances on the same host don't collide + std::string getAuthCookieName() + { + return "LmsAuth-" + LmsApp->getServerInstanceId().toString(); + } + + // changing the instance id, or this salt computation, invalidates existing "remember me" tokens + std::string getAuthCookieSalt() + { + return Wt::Auth::SHA1HashFunction{}.compute(LmsApp->getServerInstanceId().toString(), authTokenDomain.c_str()); + } void createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) { const std::string authCookie{ Wt::WRandom::generateId(64) }; - const std::string hashedAuthCookie{ Wt::Auth::SHA1HashFunction{}.compute(authCookie, authCookieSalt) }; + const std::string hashedAuthCookie{ Wt::Auth::SHA1HashFunction{}.compute(authCookie, getAuthCookieSalt()) }; core::Service::get()->createAuthToken(authTokenDomain, userId, hashedAuthCookie); - LmsApp->setCookie(authCookieName, + LmsApp->setCookie(getAuthCookieName(), authCookie, expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(), "", @@ -128,7 +138,7 @@ namespace lms::ui return Wt::WFormModel::validateField(field); } - setValidation(field, Wt::WValidator::Result(error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error)); + setValidation(field, Wt::WValidator::Result{ error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error }); return (validation(field).state() == Wt::ValidationState::Valid); } @@ -147,18 +157,18 @@ namespace lms::ui db::UserId processAuthToken(const Wt::WEnvironment& env) { - const std::string* authCookie{ env.getCookie(authCookieName) }; + const std::string* authCookie{ env.getCookie(getAuthCookieName()) }; if (!authCookie) return db::UserId{}; - const std::string hashedCookie{ Wt::Auth::SHA1HashFunction{}.compute(*authCookie, authCookieSalt) }; + const std::string hashedCookie{ Wt::Auth::SHA1HashFunction{}.compute(*authCookie, getAuthCookieSalt()) }; const auto res{ core::Service::get()->processAuthToken(authTokenDomain, boost::asio::ip::make_address(env.clientAddress()), hashedCookie) }; switch (res.state) { case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: - LmsApp->setCookie(authCookieName, std::string{}, 0, "", "", env.urlScheme() == "https"); + LmsApp->setCookie(getAuthCookieName(), std::string{}, 0, "", "", env.urlScheme() == "https"); return db::UserId{}; case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted: diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index a2523e88..df480d05 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -145,9 +145,9 @@ namespace lms::ui } // namespace - std::unique_ptr LmsApplication::create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend) + std::unique_ptr LmsApplication::create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId) { - return std::make_unique(env, db, appManager, authBackend); + return std::make_unique(env, db, appManager, authBackend, serverInstanceId); } LmsApplication* LmsApplication::instance() @@ -155,11 +155,12 @@ namespace lms::ui return static_cast(Wt::WApplication::instance()); } - LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend) + LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId) : Wt::WApplication{ env } , _db{ db } , _appManager{ appManager } , _authBackend{ authBackend } + , _serverInstanceId{ serverInstanceId } , _areDownloadsEnabled(core::Service::get()->getBool("ui-allow-downloads", true)) { try diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 3294498c..54762743 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -25,6 +25,8 @@ #include +#include "core/UUID.hpp" + #include "database/Object.hpp" #include "database/objects/Types.hpp" #include "database/objects/UserId.hpp" @@ -53,10 +55,10 @@ namespace lms::ui class LmsApplication : public Wt::WApplication { public: - LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend); - ~LmsApplication(); + LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId); + ~LmsApplication() override; - static std::unique_ptr create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend); + static std::unique_ptr create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId); static LmsApplication* instance(); // Session application data @@ -76,6 +78,7 @@ namespace lms::ui scanner::Events& getScannerEvents() { return _scannerEvents; } AuthenticationBackend getAuthBackend() const { return _authBackend; } + core::UUID getServerInstanceId() const { return _serverInstanceId; } // Utils static void post(const std::string& sessionId, const std::function& func); @@ -112,6 +115,7 @@ namespace lms::ui Wt::Signal<> _preQuit; LmsApplicationManager& _appManager; const AuthenticationBackend _authBackend; + const core::UUID _serverInstanceId; const bool _areDownloadsEnabled; scanner::Events _scannerEvents; struct UserAuthInfo diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 1cbd635c..5c3aa1f6 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -244,6 +244,7 @@ namespace lms::ui const std::string nativeResource{ _audioFileResource->getUrl(trackId) }; const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) }; + const std::string displayTitle{ utils::computeTrackDisplayInfo(track).title }; oss << "var params = {" @@ -252,7 +253,7 @@ namespace lms::ui << " transcodingResource: \"" << transcodingResource << "\"," << " duration: " << std::chrono::duration_cast>(track->getDuration()).count() << "," << " replayGain: " << replayGain << "," - << " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\"," + << " title: \"" << core::stringUtils::jsEscape(displayTitle) << "\"," << " artist: \"" << (!artistDisplayInfo.displayName.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," << " release: \"" << (release ? core::stringUtils::jsEscape(release->getName()) : "") << "\","; @@ -282,7 +283,7 @@ namespace lms::ui oss << jsRef() + ".mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay _title->setTextFormat(Wt::TextFormat::Plain); - _title->setText(Wt::WString::fromUTF8(track->getName())); + _title->setText(Wt::WString::fromUTF8(displayTitle)); _artists->clear(); _artists->addWidget(utils::createArtistsAnchors(artistDisplayInfo)); diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 9d478e89..46d3647e 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -144,7 +144,7 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; db::TrackList::pointer queue{ getQueue() }; - auto entries{ queue->getEntries().results }; + auto entries{ queue->getEntries() }; core::random::shuffleContainer(entries); queue.modify()->clear(); @@ -152,6 +152,7 @@ namespace lms::ui LmsApp->getDbSession().create(entry->getTrack(), queue); } _entriesContainer->reset(); + _nextPlayPos.reset(); addSome(); }); @@ -234,6 +235,7 @@ namespace lms::ui _entriesContainer->reset(); _trackPos.reset(); + _nextPlayPos.reset(); updateInfo(); } @@ -241,11 +243,12 @@ namespace lms::ui { updateCurrentTrack(false); _trackPos.reset(); + _nextPlayPos.reset(); _isTrackSelected = false; trackUnselected.emit(); } - void PlayQueue::loadTrack(std::size_t pos, bool play) + void PlayQueue::loadTrack(std::size_t pos, bool play, ResetNextPlayPos resetNextPlayPos) { updateCurrentTrack(false); @@ -269,6 +272,8 @@ namespace lms::ui } _trackPos = pos; + if (resetNextPlayPos.value()) + _nextPlayPos.reset(); const db::Track::pointer track{ queue->getEntry(*_trackPos)->getTrack() }; trackId = track->getId(); @@ -296,18 +301,23 @@ namespace lms::ui void PlayQueue::playNext() { - if (!_trackPos) - { - loadTrack(0, true); - return; - } - - loadTrack(*_trackPos + 1, true); + advanceTrack(ResetNextPlayPos{ true }); } void PlayQueue::onPlaybackEnded() { - playNext(); + advanceTrack(ResetNextPlayPos{ false }); + } + + void PlayQueue::advanceTrack(ResetNextPlayPos resetNextPlayPos) + { + if (!_trackPos) + { + loadTrack(0, true, resetNextPlayPos); + return; + } + + loadTrack(*_trackPos + 1, true, resetNextPlayPos); } std::size_t PlayQueue::getCount() @@ -368,7 +378,7 @@ namespace lms::ui entry->toggleStyleClass("Lms-entry-playing", selected); } - void PlayQueue::enqueueTracks(const std::vector& trackIds) + void PlayQueue::enqueueTracks(std::span trackIds) { { auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; @@ -396,64 +406,73 @@ namespace lms::ui _entriesContainer->setHasMore(); } - std::vector PlayQueue::getAndClearNextTracks() + std::vector PlayQueue::getAndClearTracksFrom(std::size_t pos) { std::vector tracks; auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; db::TrackList::pointer queue{ getQueue() }; - auto entries{ queue->getEntries(db::Range{ _trackPos ? *_trackPos + 1 : 0, getCapacity() }) }; - tracks.reserve(entries.results.size()); - for (db::TrackListEntry::pointer& entry : entries.results) + auto entries{ queue->getEntries(db::Range{ pos, getCapacity() }) }; + tracks.reserve(entries.size()); + for (db::TrackListEntry::pointer& entry : entries) { tracks.push_back(entry->getTrack()->getId()); entry.remove(); } - if (_trackPos) - { - // entries may have been cleared - if (*_trackPos + 1 < _entriesContainer->getCount()) - _entriesContainer->remove(*_trackPos + 1, _entriesContainer->getCount() - 1); - } - else + if (pos == 0) { _entriesContainer->reset(); } + else if (pos < _entriesContainer->getCount()) + { + // entries may have been cleared + _entriesContainer->remove(pos, _entriesContainer->getCount() - 1); + } return tracks; } - void PlayQueue::play(const std::vector& trackIds) + void PlayQueue::play(std::span trackIds) { playAtIndex(trackIds, 0); } - void PlayQueue::playNext(const std::vector& trackIds) + void PlayQueue::playNext(std::span trackIds) { - std::vector nextTracks{ getAndClearNextTracks() }; - nextTracks.insert(std::cbegin(nextTracks), std::cbegin(trackIds), std::cend(trackIds)); - playOrAddLast(nextTracks); + const std::size_t defaultInsertPos{ _trackPos ? *_trackPos + 1 : 0 }; + if (!_nextPlayPos || *_nextPlayPos < defaultInsertPos) + _nextPlayPos = defaultInsertPos; + + const std::size_t insertPos{ *_nextPlayPos }; + + std::vector tracksToInsert{ getAndClearTracksFrom(insertPos) }; + tracksToInsert.insert(std::cbegin(tracksToInsert), std::cbegin(trackIds), std::cend(trackIds)); + + playOrAddLast(tracksToInsert); + + // set after playOrAddLast: it may call loadTrack() (queue was empty), which resets _nextPlayPos + _nextPlayPos = insertPos + trackIds.size(); } - void PlayQueue::playShuffled(const std::vector& trackIds) + void PlayQueue::playShuffled(std::span trackIds) { clearTracks(); - std::vector shuffledTrackIds{ trackIds }; + std::vector shuffledTrackIds{ std::cbegin(trackIds), std::cend(trackIds) }; core::random::shuffleContainer(shuffledTrackIds); enqueueTracks(shuffledTrackIds); loadTrack(0, true); } - void PlayQueue::playOrAddLast(const std::vector& trackIds) + void PlayQueue::playOrAddLast(std::span trackIds) { enqueueTracks(trackIds); if (!_isTrackSelected) loadTrack(0, true); } - void PlayQueue::playAtIndex(const std::vector& trackIds, std::size_t index) + void PlayQueue::playAtIndex(std::span trackIds, std::size_t index) { clearTracks(); enqueueTracks(trackIds); @@ -466,10 +485,10 @@ namespace lms::ui const db::TrackList::pointer queue{ getQueue() }; const auto entries{ queue->getEntries(db::Range{ _entriesContainer->getCount(), _batchSize }) }; - for (const db::TrackListEntry::pointer& tracklistEntry : entries.results) + for (const db::TrackListEntry::pointer& tracklistEntry : entries) addEntry(tracklistEntry); - _entriesContainer->setHasMore(entries.moreResults); + _entriesContainer->setHasMore(entries.size() == _batchSize); } void PlayQueue::addEntry(const db::TrackListEntry::pointer& tracklistEntry) @@ -478,10 +497,17 @@ namespace lms::ui const auto track{ tracklistEntry->getTrack() }; const db::TrackId trackId{ track->getId() }; + const utils::TrackDisplayInfo displayInfo{ utils::computeTrackDisplayInfo(track) }; + Template* entry{ _entriesContainer->addNew