Merge branch 'develop' for release v3.79.0

This commit is contained in:
emeric
2026-07-18 12:03:18 +02:00
230 changed files with 5425 additions and 2590 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12) 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/) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
+2 -1
View File
@@ -16,6 +16,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* [ListenBrainz](https://listenbrainz.org) support for: * [ListenBrainz](https://listenbrainz.org) support for:
* Scrobbling and synchronizing listens * Scrobbling and synchronizing listens
* Synchronizing 'love' feedbacks * Synchronizing 'love' feedbacks
* [Last.fm](https://www.last.fm) support for scrobbling listens
* Recommendation engine * Recommendation engine
* Multi-library support * Multi-library support
* ReplayGain 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: __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`. * **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 ## 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). _LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md).
+5 -7
View File
@@ -17,13 +17,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
## Authentication ## 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. _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.
If a client's login screen has no dedicated API key field, enter the API key as the password instead.
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).
## Extra fields ## Extra fields
The following extra fields are implemented: The following extra fields are implemented:
@@ -33,6 +27,7 @@ The following extra fields are implemented:
* `displayArtist` * `displayArtist`
* `explicitStatus` * `explicitStatus`
* `genres` * `genres`
* `groupings`
* `isCompilation` * `isCompilation`
* `played` * `played`
* `mediaType` * `mediaType`
@@ -56,12 +51,15 @@ The following extra fields are implemented:
* `displayArtist` * `displayArtist`
* `explicitStatus` * `explicitStatus`
* `genres` * `genres`
* `groupings`
* `mediaType` * `mediaType`
* `moods` * `moods`
* `movements`
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
* `played` * `played`
* `replayGain` * `replayGain`
* `samplingRate` * `samplingRate`
* `works`
* `Artist` response: * `Artist` response:
* `mediaType` * `mediaType`
* `musicBrainzId` * `musicBrainzId`
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">Discard</message> <message id="Lms.discard">Discard</message>
<message id="Lms.edit">Edit</message> <message id="Lms.edit">Edit</message>
<message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message> <message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message>
<message id="Lms.infinite-scrolling-container.limit-reached">Display limit reached — refine your search to see more results</message>
<message id="Lms.infinite-scrolling-container.no-results">No results found</message>
<message id="Lms.loading">Loading...</message> <message id="Lms.loading">Loading...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">Descartar</message> <message id="Lms.discard">Descartar</message>
<message id="Lms.edit">Editar</message> <message id="Lms.edit">Editar</message>
<message id="Lms.field-must-be-in-upper-case">Este campo debe estar en mayúsculas</message> <message id="Lms.field-must-be-in-upper-case">Este campo debe estar en mayúsculas</message>
<message id="Lms.infinite-scrolling-container.limit-reached">Límite de visualización alcanzado — refine su búsqueda para ver más resultados</message>
<message id="Lms.infinite-scrolling-container.no-results">No se han encontrado resultados</message>
<message id="Lms.loading">Cargando...</message> <message id="Lms.loading">Cargando...</message>
<message id="Lms.login">Usuario</message> <message id="Lms.login">Usuario</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Salir</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Salir</message>
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">Annuler</message> <message id="Lms.discard">Annuler</message>
<message id="Lms.edit">Editer</message> <message id="Lms.edit">Editer</message>
<message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message> <message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message>
<message id="Lms.infinite-scrolling-container.limit-reached">Limite d'affichage atteinte — affinez votre recherche pour voir plus de résultats</message>
<message id="Lms.infinite-scrolling-container.no-results">Aucun résultat</message>
<message id="Lms.loading">Chargement...</message> <message id="Lms.loading">Chargement...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">Annulla</message> <message id="Lms.discard">Annulla</message>
<message id="Lms.edit">Modifica</message> <message id="Lms.edit">Modifica</message>
<message id="Lms.field-must-be-in-upper-case">Questo campo deve essere in maiuscolo</message> <message id="Lms.field-must-be-in-upper-case">Questo campo deve essere in maiuscolo</message>
<message id="Lms.infinite-scrolling-container.limit-reached">Limite di visualizzazione raggiunto — affina la ricerca per vedere più risultati</message>
<message id="Lms.infinite-scrolling-container.no-results">Nessun risultato trovato</message>
<message id="Lms.loading">Caricamento...</message> <message id="Lms.loading">Caricamento...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">Odrzuć</message> <message id="Lms.discard">Odrzuć</message>
<message id="Lms.edit">Zmień</message> <message id="Lms.edit">Zmień</message>
<message id="Lms.field-must-be-in-upper-case">Wartość musi się zaczynać wielką literą</message> <message id="Lms.field-must-be-in-upper-case">Wartość musi się zaczynać wielką literą</message>
<message id="Lms.infinite-scrolling-container.limit-reached">Osiągnięto limit wyświetlania — doprecyzuj wyszukiwanie, aby zobaczyć więcej wyników</message>
<message id="Lms.infinite-scrolling-container.no-results">Brak wyników</message>
<message id="Lms.loading">Wczytywanie...</message> <message id="Lms.loading">Wczytywanie...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Wyloguj</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Wyloguj</message>
+2
View File
@@ -14,6 +14,8 @@
<message id="Lms.discard">撤销</message> <message id="Lms.discard">撤销</message>
<message id="Lms.edit">编辑</message> <message id="Lms.edit">编辑</message>
<message id="Lms.field-must-be-in-upper-case">此字段必须为大写</message> <message id="Lms.field-must-be-in-upper-case">此字段必须为大写</message>
<message id="Lms.infinite-scrolling-container.limit-reached">已达到显示上限 — 请精简搜索条件以查看更多结果</message>
<message id="Lms.infinite-scrolling-container.no-results">未找到结果</message>
<message id="Lms.loading">加载中...</message> <message id="Lms.loading">加载中...</message>
<message id="Lms.login">登陆</message> <message id="Lms.login">登陆</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
+1
View File
@@ -9,6 +9,7 @@
</div> </div>
</message> </message>
<message id="Lms.infinite-scrolling-container.template"> <message id="Lms.infinite-scrolling-container.template">
${elements} ${elements}
${loading-indicator} ${loading-indicator}
+8 -7
View File
@@ -40,18 +40,19 @@
<div class="p-1"> <div class="p-1">
${cover} ${cover}
</div> </div>
<div class="row align-items-center flex-fill overflow-hidden"> <div class="row align-items-center flex-fill">
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<div class="p-2 overflow-hidden"> <div class="p-2">
<div class="d-block text-truncate">${name}</div> <div class="d-block">${name}</div>
${<if-has-artists>}${artists class="d-block d-md-none text-truncate"}${</if-has-artists>} ${<if-has-artists>}${artists class="d-block d-md-none"}${</if-has-artists>}
${<if-has-work>}<div class="text-muted small ms-2">${work}</div>${</if-has-work>}
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
${<if-has-artists>}${artists-md class="d-none d-md-block text-truncate"}${</if-has-artists>} ${<if-has-artists>}${artists-md class="d-none d-md-block"}${</if-has-artists>}
</div> </div>
<div class="col-lg-4 text-truncate"> <div class="col-lg-4">
${<if-has-release>}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${</if-has-release>} ${<if-has-release>}${release class="d-none d-lg-inline text-decoration-none link-success"}${</if-has-release>}
</div> </div>
</div> </div>
<div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration"> <div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration">
+28 -21
View File
@@ -55,36 +55,43 @@
${artist class="text-decoration-none link-success"} ${artist class="text-decoration-none link-success"}
</message> </message>
<message id="Lms.Explore.Release.template.entry-disc"> <message id="Lms.Explore.Release.template.segment-header">
<h4 class="d-flex align-items-center"> ${<if-has-artwork>}
${<if-has-artwork>} <div class="p-1">${artwork class="Lms-cursor-pointer"}</div>
<div class="p-1">${artwork class="Lms-cursor-pointer"}</div> ${</if-has-artwork>}
${</if-has-artwork>} <div class="p-2 flex-fill">${title}</div>
<div class="p-2 flex-fill">${disc-title}</div> <div class="p-2 d-flex">
<div class="p-2 d-flex"> ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} <div class="dropdown d-inline-block">
<div class="dropdown d-inline-block"> ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"} <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}"> <li class="d-block d-sm-none">${play class="dropdown-item"}</li>
<li class="d-block d-sm-none">${play class="dropdown-item"}</li> <li>${play-next class="dropdown-item"}</li>
<li>${play-next class="dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li> <li>${play-shuffled class="dropdown-item"}</li>
<li>${play-shuffled class="dropdown-item"}</li> </ul>
</ul>
</div>
</div> </div>
</h4> </div>
</message>
<message id="Lms.Explore.Release.template.segment-disc">
<h4>${header class="d-flex align-items-center"}</h4>
${tracks class="d-grid gap-1"}
</message>
<message id="Lms.Explore.Release.template.segment-work">
<h5 class="text-muted">${header class="d-flex align-items-center"}</h5>
${tracks class="d-grid gap-1 Lms-row-container"} ${tracks class="d-grid gap-1 Lms-row-container"}
</message> </message>
<message id="Lms.Explore.Release.template.entry-nodisc"> <message id="Lms.Explore.Release.template.segment-flat">
${tracks class="d-grid gap-1 Lms-row-container"} ${tracks class="d-grid gap-1 Lms-row-container"}
</message> </message>
<message id="Lms.Explore.Release.template.entry"> <message id="Lms.Explore.Release.template.track">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="p-2 text-muted text-end Lms-tracks-entry-position"> <div class="p-2 text-muted text-end Lms-tracks-entry-position">
${<if-has-track-number>}${track-number}${</if-has-track-number>} ${<if-has-position>}${position}${</if-has-position>}
</div> </div>
<div class="row align-items-center flex-fill overflow-hidden"> <div class="row align-items-center flex-fill overflow-hidden">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
-2
View File
@@ -54,11 +54,9 @@
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header">
${tr:Lms.Settings.subsonic-token} ${tr:Lms.Settings.subsonic-token}
${<if-has-subsonic-token-usage>}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.subsonic-token-usage}"> <button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.subsonic-token-usage}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i> <i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
</button> </button>
${</if-has-subsonic-token-usage>}
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="input-group mb-3"> <div class="input-group mb-3">
+8 -7
View File
@@ -32,18 +32,19 @@
<div class="p-1"> <div class="p-1">
${cover} ${cover}
</div> </div>
<div class="row align-items-center flex-fill overflow-hidden"> <div class="row align-items-center flex-fill">
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<div class="p-2 overflow-hidden"> <div class="p-2">
<div class="d-block text-truncate">${name}</div> <div class="d-block">${name}</div>
${<if-has-artists>}${artists class="d-block d-md-none text-truncate"}${</if-has-artists>} ${<if-has-artists>}${artists class="d-block d-md-none"}${</if-has-artists>}
${<if-has-work>}<div class="text-muted small ms-2">${work}</div>${</if-has-work>}
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
${<if-has-artists>}${artists-md class="d-none d-md-block text-truncate"}${</if-has-artists>} ${<if-has-artists>}${artists-md class="d-none d-md-block"}${</if-has-artists>}
</div> </div>
<div class="col-lg-4 text-truncate"> <div class="col-lg-4">
${<if-has-release>}${release class="d-none d-lg-inline text-truncate text-decoration-none link-success"}${</if-has-release>} ${<if-has-release>}${release class="d-none d-lg-inline text-decoration-none link-success"}${</if-has-release>}
</div> </div>
</div> </div>
<div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration"> <div class="p-2 d-none d-sm-block text-muted text-center Lms-tracks-entry-duration">
-8
View File
@@ -84,14 +84,6 @@ login-throttler-max-entries = 10000;
# API # API
api-subsonic = true; 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 # List of clients for whom open subsonic extensions and extra fields are disabled
api-open-subsonic-disabled-clients = ("DSub"); api-open-subsonic-disabled-clients = ("DSub");
+3 -3
View File
@@ -65,7 +65,7 @@ namespace lms::audio::ffmpeg
return planar ? AV_SAMPLE_FMT_DBLP : AV_SAMPLE_FMT_DBL; 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) SwrContextPtr createResampler(const PcmParameters& params, const AVChannelLayout& inLayout, AVSampleFormat inFmt, int inSampleRate)
@@ -104,7 +104,7 @@ namespace lms::audio::ffmpeg
: _parameters{ parameters } : _parameters{ parameters }
{ {
if (_parameters.channelCount > AV_NUM_DATA_POINTERS) 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(); utils::init();
@@ -179,7 +179,7 @@ namespace lms::audio::ffmpeg
{ {
int error{ ::avcodec_open2(_decoderContext.get(), decoder, nullptr) }; int error{ ::avcodec_open2(_decoderContext.get(), decoder, nullptr) };
if (error < 0) if (error < 0)
throw FFmpegException("Cannot open decoder", error); throw FFmpegException{ "Cannot open decoder", error };
} }
_decodedFrame = AVFramePtr{ av_frame_alloc() }; _decodedFrame = AVFramePtr{ av_frame_alloc() };
+3 -3
View File
@@ -83,9 +83,9 @@ namespace lms::audio::ffmpeg
{ TagType::Mixers, { "MIXERS" } }, { TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } }, { TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } }, { TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } }, { TagType::Movement, { "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } }, { TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } }, { TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } }, { TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } }, { TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } }, { TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
@@ -89,13 +89,13 @@ namespace lms::audio::pulseaudio
{ {
const int error{ ::pa_context_connect(_context.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) }; const int error{ ::pa_context_connect(_context.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr) };
if (error < 0) 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()) }; const int error{ ::pa_threaded_mainloop_start(_mainLoop.get()) };
if (error < 0) if (error < 0)
throw PaException("pa_threaded_mainloop_start failed", error); throw PaException{ "pa_threaded_mainloop_start failed", error };
} }
} }
@@ -226,7 +226,7 @@ namespace lms::audio::pulseaudio
{ {
pa_operation* op{ ::pa_stream_flush(_stream.get(), nullptr, nullptr) }; pa_operation* op{ ::pa_stream_flush(_stream.get(), nullptr, nullptr) };
if (!op) 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); ::pa_operation_unref(op);
} }
@@ -249,7 +249,7 @@ namespace lms::audio::pulseaudio
pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) }; pa_operation* op{ ::pa_stream_cork(_stream.get(), 1, nullptr, nullptr) };
if (!op) 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); ::pa_operation_unref(op);
} }
@@ -263,14 +263,14 @@ namespace lms::audio::pulseaudio
{ {
pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) }; pa_operation* op{ ::pa_stream_cork(_stream.get(), 0, nullptr, nullptr) };
if (!op) 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_unref(op);
} }
{ {
pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) }; pa_operation* op{ ::pa_stream_trigger(_stream.get(), NULL, NULL) };
if (!op) 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); ::pa_operation_unref(op);
} }
} }
@@ -301,7 +301,7 @@ namespace lms::audio::pulseaudio
nullptr, nullptr,
nullptr) }; nullptr) };
if (!op) 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); ::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<AudioOutputStream*>(userdata)->onDrainComplete(success); }, this) }; ::pa_operation* op{ ::pa_stream_drain(_stream.get(), [](pa_stream*, int success, void* userdata) { static_cast<AudioOutputStream*>(userdata)->onDrainComplete(success); }, this) };
if (!op) 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); ::pa_operation_unref(op);
} }
+3 -3
View File
@@ -113,9 +113,9 @@ namespace lms::audio::taglib
{ TagType::Mixers, { "MIXERS" } }, { TagType::Mixers, { "MIXERS" } },
{ TagType::MixersSortOrder, { "MIXERSSORT" } }, { TagType::MixersSortOrder, { "MIXERSSORT" } },
{ TagType::Mood, { "MOOD" } }, { TagType::Mood, { "MOOD" } },
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } }, { TagType::Movement, { "MOVEMENTNAME" } },
{ TagType::MovementCount, { "MOVEMENTCOUNT" } }, { TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } },
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } }, { TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } },
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } }, { TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } }, { TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } }, { TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
+1
View File
@@ -13,6 +13,7 @@ configure_file(
add_library(lmscore STATIC add_library(lmscore STATIC
impl/http/Client.cpp impl/http/Client.cpp
impl/http/SendQueue.cpp impl/http/SendQueue.cpp
impl/http/UrlValidation.cpp
impl/media/Codec.cpp impl/media/Codec.cpp
impl/media/Container.cpp impl/media/Container.cpp
impl/media/ImageType.cpp impl/media/ImageType.cpp
+53 -16
View File
@@ -341,6 +341,15 @@ namespace lms::core::stringUtils
return str.substr(0, str.find_last_not_of(whitespaces) + 1); 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<unsigned char>(str[len]) & 0xC0) == 0x80)
--len;
return str.substr(0, len);
}
std::string stringToLower(std::string_view str) std::string stringToLower(std::string_view str)
{ {
std::string res; std::string res;
@@ -366,18 +375,6 @@ namespace lms::core::stringUtils
return res; return res;
} }
std::string bufferToString(std::span<const unsigned char> 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) bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB)
{ {
if (strA.size() != strB.size()) 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 replaceInString(std::string_view str, std::string_view from, std::string_view to)
{ {
std::string res{ str }; std::string res{ str };
@@ -561,16 +596,18 @@ namespace lms::core::stringUtils
return res; return res;
} }
std::string toHexString(std::string_view str) std::string bufferToHexString(std::span<const std::byte> data)
{ {
constexpr char lut[]{ "0123456789ABCDEF" }; constexpr char lut[]{ "0123456789ABCDEF" };
std::string res; 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]); const unsigned value{ std::to_integer<unsigned>(b) };
res.push_back(lut[c & 0xF]); res.push_back(lut[(value >> 4) & 0xF]);
res.push_back(lut[value & 0xF]);
} }
return res; return res;
+7
View File
@@ -31,6 +31,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/http/UrlValidation.hpp"
#define LOG(sev, message) LMS_LOG(HTTP, sev, "[Http SendQueue] - " << message) #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 }; const std::string url{ _baseUrl + request.getParameters().relativeUrl };
LOG(DEBUG, "Sending " << (request.getType() == ClientRequest::Type::GET ? "GET" : "POST") << " request to url '" << url << "'"); 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); _client.setMaximumResponseSize(request.getParameters().onChunkReceived ? 0 : request.getParameters().responseBufferSize);
bool res{}; bool res{};
+28
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
+1 -1
View File
@@ -40,7 +40,7 @@ namespace lms::core::media
CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false }, CodecDesc{ Codec::MPC7, "MPC7", "Musepack7", false },
CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false }, CodecDesc{ Codec::MPC8, "MPC8", "Musepack8", false },
CodecDesc{ Codec::Opus, "Opus", "Opus", 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::Shorten, "Shorten", "Shorten", true },
CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true }, CodecDesc{ Codec::TrueAudio, "TTA", "The True Audio", true },
CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false }, CodecDesc{ Codec::Vorbis, "Vorbis", "Vorbis", false },
+8 -3
View File
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <cstddef>
#include <optional> #include <optional>
#include <span> #include <span>
#include <sstream> #include <sstream>
@@ -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 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"); [[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); [[nodiscard]] std::string stringToLower(std::string_view str);
void stringToLower(std::string& str); void stringToLower(std::string& str);
[[nodiscard]] std::string stringToUpper(const std::string& str); [[nodiscard]] std::string stringToUpper(const std::string& str);
[[nodiscard]] std::string bufferToString(std::span<const unsigned char> data);
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB); [[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); [[nodiscard]] std::string_view::size_type stringCaseInsensitiveContains(std::string_view str, std::string_view strtoFind);
void capitalize(std::string& str); void capitalize(std::string& str);
// returns empty string if invalid input
[[nodiscard]] std::string toRomanNumeral(std::size_t n);
template<typename T> template<typename T>
[[nodiscard]] std::optional<T> readAs(std::string_view str) [[nodiscard]] std::optional<T> 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]] bool stringEndsWith(std::string_view str, std::string_view ending);
[[nodiscard]] std::optional<std::string> stringFromHex(std::string_view str); [[nodiscard]] std::optional<std::string> stringFromHex(std::string_view str);
[[nodiscard]] std::string toHexString(std::string_view str); [[nodiscard]] std::string bufferToHexString(std::span<const std::byte> data);
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime); [[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date); [[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
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
+1
View File
@@ -2,6 +2,7 @@ include(GoogleTest)
add_executable(test-core add_executable(test-core
EnumSet.cpp EnumSet.cpp
HttpUrlValidation.cpp
JobScheduler.cpp JobScheduler.cpp
LiteralString.cpp LiteralString.cpp
PartialDateTime.cpp PartialDateTime.cpp
+43
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#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
+60 -6
View File
@@ -17,6 +17,9 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <array>
#include <limits>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <Wt/WDate.h> #include <Wt/WDate.h>
@@ -385,6 +388,31 @@ namespace lms::core::stringUtils::tests
EXPECT_FALSE(stringEndsWith("FooBar", "R")); 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) TEST(StringUtils, stringCaseInsensitiveContains)
{ {
EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "Bar")); EXPECT_TRUE(stringCaseInsensitiveContains("FooBar", "Bar"));
@@ -397,13 +425,11 @@ namespace lms::core::stringUtils::tests
EXPECT_FALSE(stringCaseInsensitiveContains("", "Foo")); EXPECT_FALSE(stringCaseInsensitiveContains("", "Foo"));
} }
TEST(StringUtils, toHexString) TEST(StringUtils, bufferToHexString)
{ {
EXPECT_EQ(toHexString(""), ""); EXPECT_EQ(bufferToHexString({}), "");
EXPECT_EQ(toHexString("123"), "313233"); EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x31 }, std::byte{ 0x32 }, std::byte{ 0x33 } }), "313233");
EXPECT_EQ(toHexString("1234"), "31323334"); EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x00 }, std::byte{ 0xab }, std::byte{ 0xcd }, std::byte{ 0xff } }), "00ABCDFF");
EXPECT_EQ(toHexString("12345"), "3132333435");
EXPECT_EQ(toHexString("Test"), "54657374");
// test back stringFromHex // test back stringFromHex
EXPECT_EQ(stringFromHex(""), ""); EXPECT_EQ(stringFromHex(""), "");
@@ -412,4 +438,32 @@ namespace lms::core::stringUtils::tests
EXPECT_EQ(stringFromHex("3132333435"), "12345"); EXPECT_EQ(stringFromHex("3132333435"), "12345");
EXPECT_EQ(stringFromHex("54657374"), "Test"); 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<std::size_t>(-1)), ""); // underflows to SIZE_MAX
EXPECT_EQ(toRomanNumeral(std::numeric_limits<std::size_t>::max()), "");
}
} // namespace lms::core::stringUtils::tests } // namespace lms::core::stringUtils::tests
+3
View File
@@ -24,11 +24,14 @@ add_library(lmsdatabase STATIC
impl/objects/RatedTrack.cpp impl/objects/RatedTrack.cpp
impl/objects/Release.cpp impl/objects/Release.cpp
impl/objects/ScanSettings.cpp impl/objects/ScanSettings.cpp
impl/objects/ServerInfo.cpp
impl/objects/StarredArtist.cpp impl/objects/StarredArtist.cpp
impl/objects/StarredRelease.cpp impl/objects/StarredRelease.cpp
impl/objects/StarredTrack.cpp impl/objects/StarredTrack.cpp
impl/objects/Movement.cpp
impl/objects/Track.cpp impl/objects/Track.cpp
impl/objects/TrackArtistLink.cpp impl/objects/TrackArtistLink.cpp
impl/objects/Work.cpp
impl/objects/TrackBookmark.cpp impl/objects/TrackBookmark.cpp
impl/objects/TrackEmbeddedImage.cpp impl/objects/TrackEmbeddedImage.cpp
impl/objects/TrackEmbeddedImageLink.cpp impl/objects/TrackEmbeddedImageLink.cpp
+2 -2
View File
@@ -248,7 +248,7 @@ namespace lms::db
} }
else if (checkType != "none") 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) 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!"); LMS_LOG(DB, INFO, "Foreign key constraints check passed!");
} }
+47 -1
View File
@@ -36,7 +36,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 108 }; static constexpr Version LMS_DATABASE_VERSION{ 110 };
} }
VersionInfo::VersionInfo() 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'))"); 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) bool doDbMigration(Session& session)
{ {
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; 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 }, { 105, migrateFromV105 },
{ 106, migrateFromV106 }, { 106, migrateFromV106 },
{ 107, migrateFromV107 }, { 107, migrateFromV107 },
{ 108, migrateFromV108 },
{ 109, migrateFromV109 },
}; };
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration"); LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
+23 -8
View File
@@ -38,6 +38,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/PlayQueue.hpp" #include "database/objects/PlayQueue.hpp"
#include "database/objects/Podcast.hpp" #include "database/objects/Podcast.hpp"
@@ -48,6 +49,7 @@
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/ServerInfo.hpp"
#include "database/objects/StarredArtist.hpp" #include "database/objects/StarredArtist.hpp"
#include "database/objects/StarredRelease.hpp" #include "database/objects/StarredRelease.hpp"
#include "database/objects/StarredTrack.hpp" #include "database/objects/StarredTrack.hpp"
@@ -61,6 +63,7 @@
#include "database/objects/TrackMusicNNEmbeddings.hpp" #include "database/objects/TrackMusicNNEmbeddings.hpp"
#include "database/objects/UIState.hpp" #include "database/objects/UIState.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "database/objects/Work.hpp"
#include "Db.hpp" #include "Db.hpp"
#include "Migration.hpp" #include "Migration.hpp"
@@ -111,6 +114,7 @@ namespace lms::db
_session.mapClass<StarredRelease>("starred_release"); _session.mapClass<StarredRelease>("starred_release");
_session.mapClass<StarredTrack>("starred_track"); _session.mapClass<StarredTrack>("starred_track");
_session.mapClass<Track>("track"); _session.mapClass<Track>("track");
_session.mapClass<Movement>("track_movement");
_session.mapClass<TrackBookmark>("track_bookmark"); _session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link"); _session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackEmbeddedImage>("track_embedded_image"); _session.mapClass<TrackEmbeddedImage>("track_embedded_image");
@@ -120,7 +124,9 @@ namespace lms::db
_session.mapClass<TrackListEntry>("tracklist_entry"); _session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<TrackLyrics>("track_lyrics"); _session.mapClass<TrackLyrics>("track_lyrics");
_session.mapClass<UIState>("ui_state"); _session.mapClass<UIState>("ui_state");
_session.mapClass<Work>("work");
_session.mapClass<User>("user"); _session.mapClass<User>("user");
_session.mapClass<ServerInfo>("server_info");
_session.mapClass<VersionInfo>("version_info"); _session.mapClass<VersionInfo>("version_info");
} }
@@ -176,17 +182,22 @@ namespace lms::db
bool Session::migrateSchemaIfNeeded() bool Session::migrateSchemaIfNeeded()
{ {
const bool migrationPerformed{ Migration::doDbMigration(*this) }; return Migration::doDbMigration(*this);
}
// TODO: move this elsewhere void Session::createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType)
{ {
auto uniqueTransaction{ createWriteTransaction() }; auto uniqueTransaction{ createWriteTransaction() };
if (!ScanSettings::find(*this)) if (!ScanSettings::find(*this))
create<ScanSettings>(); create<ScanSettings>().modify()->setRecommendationEngineType(defaultRecommendationEngineType);
} }
return migrationPerformed; void Session::createServerInfoIfNeeded()
{
auto uniqueTransaction{ createWriteTransaction() };
ServerInfo::getOrCreate(*this);
} }
void Session::createIndexesIfNeeded() 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_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_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_idx ON track_bookmark(user_id)",
"CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)", "CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)",
+3 -51
View File
@@ -128,29 +128,10 @@ namespace lms::db::utils
} }
template<typename ResultType, typename Query> template<typename ResultType, typename Query>
RangeResults<ResultType> execRangeQuery(Query& query, const std::optional<Range> range) std::vector<ResultType> execRangeQuery(Query& query, const std::optional<Range> range)
{ {
RangeResults<ResultType> res; applyRange(query, range);
return utils::fetchQueryResults<ResultType>(query);
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<ResultType>(query);
if (range && (res.results.size() == range->size + 1))
{
res.moreResults = true;
res.results.pop_back();
}
res.range.size = res.results.size();
return res;
} }
template<typename Query, typename UnaryFunc> template<typename Query, typename UnaryFunc>
@@ -162,35 +143,6 @@ namespace lms::db::utils
forEachQueryResult(query, std::forward<UnaryFunc>(func)); forEachQueryResult(query, std::forward<UnaryFunc>(func));
} }
template<typename Query, typename UnaryFunc>
void forEachQueryRangeResult(Query& query, std::optional<Range> range, bool& moreResults, UnaryFunc&& func)
{
using ResultType = typename QueryResultType<Query>::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<std::size_t>(range->size)))
{
moreResults = true;
break;
}
queryProfiler.suspend();
func(*it);
queryProfiler.resume();
fetchNextResult<ResultType>(it);
}
}
template<typename... Args> template<typename... Args>
void executeCommand(Wt::Dbo::Session& session, std::string_view command, const Args&... args) void executeCommand(Wt::Dbo::Session& session, std::string_view command, const Args&... args)
{ {
+17 -17
View File
@@ -22,6 +22,8 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
@@ -135,14 +137,13 @@ namespace lms::db
if (params.starringUser.isValid()) if (params.starringUser.isValid())
{ {
assert(params.feedbackBackend);
query.join("starred_artist s_a ON s_a.artist_id = a.id") 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 = ?") .where("s_a.user_id = ?")
.bind(params.starringUser) .bind(params.starringUser)
.where("s_a.backend = ?")
.bind(*params.feedbackBackend)
.where("s_a.sync_state <> ?") .where("s_a.sync_state <> ?")
.bind(SyncState::PendingRemove); .bind(SyncState::PendingRemove)
.where("s_a.backend = u.feedback_backend");
} }
if (params.filters.clusters.size() == 1) if (params.filters.clusters.size() == 1)
@@ -160,7 +161,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId clusterId : params.filters.clusters) for (const ClusterId clusterId : params.filters.clusters)
{ {
clusterClause.Or(WhereClause("t_c.cluster_id = ?")); clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" });
query.bind(clusterId); query.bind(clusterId);
} }
@@ -305,8 +306,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
if (name.size() > maxNameLength) name = core::stringUtils::utf8Truncate(name, maxNameLength);
name = name.substr(0, maxNameLength);
return utils::fetchQueryResults<Artist::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.name = ?").bind(name).orderBy("LENGTH(a.mbid) DESC")); // put mbid entries first return utils::fetchQueryResults<Artist::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("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<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.id = ?").bind(id));
} }
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params) std::vector<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -331,7 +331,7 @@ namespace lms::db
return utils::execRangeQuery<ArtistId>(query, params.range); return utils::execRangeQuery<ArtistId>(query, params.range);
} }
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params) std::vector<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -357,7 +357,7 @@ namespace lms::db
return IdRange<ArtistId>{ .first = std::get<0>(res), .last = std::get<1>(res) }; return IdRange<ArtistId>{ .first = std::get<0>(res), .last = std::get<1>(res) };
} }
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range) std::vector<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
{ {
// TODO extend with release artists // TODO extend with release artists
session.checkReadTransaction(); session.checkReadTransaction();
@@ -388,7 +388,7 @@ AND NOT EXISTS (
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1; return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1;
} }
RangeResults<Artist::pointer> Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range) std::vector<Artist::pointer> Artist::findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -407,8 +407,8 @@ AND NOT EXISTS (
auto results{ utils::execRangeQuery<Artist::pointer>(query, range) }; auto results{ utils::execRangeQuery<Artist::pointer>(query, range) };
if (!results.results.empty()) if (!results.empty())
lastRetrievedArtist = results.results.back()->getId(); lastRetrievedArtist = results.back()->getId();
return results; return results;
} }
@@ -442,11 +442,11 @@ AND NOT EXISTS (
std::ostringstream oss; 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"; 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; WhereClause clusterClause;
for (const ClusterTypeId clusterTypeId : clusterTypeIds) 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); where.And(clusterClause);
} }
@@ -474,13 +474,13 @@ AND NOT EXISTS (
void Artist::setName(std::string_view name) 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 << "'"); LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Artist name too long, truncated to '" << _name << "'");
} }
void Artist::setSortName(std::string_view sortName) 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 << "'"); LMS_LOG_IF(DB, WARNING, sortName.size() > maxNameLength, "Artist sort name too long, truncated to '" << _sortName << "'");
} }
+9 -7
View File
@@ -31,12 +31,14 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -130,7 +132,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster"));
} }
RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params) std::vector<ClusterId> Cluster::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<ClusterId>(session, params) }; auto query{ createQuery<ClusterId>(session, params) };
@@ -138,7 +140,7 @@ namespace lms::db
return utils::execRangeQuery<ClusterId>(query, params.range); return utils::execRangeQuery<ClusterId>(query, params.range);
} }
RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params) std::vector<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
@@ -154,7 +156,7 @@ namespace lms::db
return utils::forEachQueryResult(query, _func); return utils::forEachQueryResult(query, _func);
} }
RangeResults<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range) std::vector<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; auto query{ session.getDboSession()->query<ClusterId>("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)); _tracks.insert(getDboPtr(track));
} }
RangeResults<TrackId> Cluster::getTracks(std::optional<Range> range) const std::vector<TrackId> Cluster::getTracks(std::optional<Range> range) const
{ {
assert(session()); assert(session());
@@ -217,7 +219,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster_type")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster_type"));
} }
RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range) std::vector<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -229,7 +231,7 @@ namespace lms::db
return utils::execRangeQuery<ClusterTypeId>(query, range); return utils::execRangeQuery<ClusterTypeId>(query, range);
} }
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range) std::vector<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -263,7 +265,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("id = ?").bind(id));
} }
RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range) std::vector<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+4 -4
View File
@@ -173,7 +173,7 @@ namespace lms::db
}); });
} }
RangeResults<Directory::pointer> Directory::find(Session& session, const FindParameters& params) std::vector<Directory::pointer> Directory::find(Session& session, const FindParameters& params)
{ {
auto query{ createQuery(session, params) }; auto query{ createQuery(session, params) };
return utils::execRangeQuery<Directory::pointer>(query, params.range); return utils::execRangeQuery<Directory::pointer>(query, params.range);
@@ -187,7 +187,7 @@ namespace lms::db
}); });
} }
RangeResults<DirectoryId> Directory::findOrphanIds(Session& session, std::optional<Range> range) std::vector<DirectoryId> Directory::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -208,7 +208,7 @@ namespace lms::db
return utils::execRangeQuery<DirectoryId>(query, range); return utils::execRangeQuery<DirectoryId>(query, range);
} }
RangeResults<DirectoryId> Directory::findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId) std::vector<DirectoryId> Directory::findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -219,7 +219,7 @@ namespace lms::db
return utils::execRangeQuery<DirectoryId>(query, range); return utils::execRangeQuery<DirectoryId>(query, range);
} }
RangeResults<Directory::pointer> Directory::findRootDirectories(Session& session, std::optional<Range> range) std::vector<Directory::pointer> Directory::findRootDirectories(Session& session, std::optional<Range> range)
{ {
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.parent_directory_id IS NULL") }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.parent_directory_id IS NULL") };
return utils::execRangeQuery<Directory::pointer>(query, range); return utils::execRangeQuery<Directory::pointer>(query, range);
+8 -6
View File
@@ -22,6 +22,7 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -33,12 +34,14 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -112,7 +115,7 @@ namespace lms::db
} // namespace } // namespace
Genre::Genre(std::string_view name) 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 << "'"); 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<int>("SELECT COUNT(*) FROM genre")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM genre"));
} }
RangeResults<GenreId> Genre::findIds(Session& session, const FindParameters& params) std::vector<GenreId> Genre::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<GenreId>(session, params) }; auto query{ createQuery<GenreId>(session, params) };
return utils::execRangeQuery<GenreId>(query, params.range); return utils::execRangeQuery<GenreId>(query, params.range);
} }
RangeResults<Genre::pointer> Genre::find(Session& session, const FindParameters& params) std::vector<Genre::pointer> Genre::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) };
@@ -159,13 +162,12 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
if (name.size() > maxNameLength) name = core::stringUtils::utf8Truncate(name, maxNameLength);
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("name = ?").bind(name));
} }
RangeResults<GenreId> Genre::findOrphanIds(Session& session, std::optional<Range> range) std::vector<GenreId> Genre::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<GenreId>("SELECT g.id FROM genre g WHERE NOT EXISTS (SELECT 1 FROM track_genre t_g WHERE t_g.genre_id = g.id)") }; auto query{ session.getDboSession()->query<GenreId>("SELECT g.id FROM genre g WHERE NOT EXISTS (SELECT 1 FROM track_genre t_g WHERE t_g.genre_id = g.id)") };
+8 -6
View File
@@ -22,6 +22,7 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -33,12 +34,14 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -112,7 +115,7 @@ namespace lms::db
} // namespace } // namespace
Grouping::Grouping(std::string_view name) 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 << "'"); 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<int>("SELECT COUNT(*) FROM grouping")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM grouping"));
} }
RangeResults<GroupingId> Grouping::findIds(Session& session, const FindParameters& params) std::vector<GroupingId> Grouping::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<GroupingId>(session, params) }; auto query{ createQuery<GroupingId>(session, params) };
return utils::execRangeQuery<GroupingId>(query, params.range); return utils::execRangeQuery<GroupingId>(query, params.range);
} }
RangeResults<Grouping::pointer> Grouping::find(Session& session, const FindParameters& params) std::vector<Grouping::pointer> Grouping::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) };
@@ -159,13 +162,12 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
if (name.size() > maxNameLength) name = core::stringUtils::utf8Truncate(name, maxNameLength);
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("name = ?").bind(name));
} }
RangeResults<GroupingId> Grouping::findOrphanIds(Session& session, std::optional<Range> range) std::vector<GroupingId> Grouping::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<GroupingId>("SELECT g.id FROM grouping g WHERE NOT EXISTS (SELECT 1 FROM track_grouping t_gr WHERE t_gr.grouping_id = g.id)") }; auto query{ session.getDboSession()->query<GroupingId>("SELECT g.id FROM grouping g WHERE NOT EXISTS (SELECT 1 FROM track_grouping t_gr WHERE t_gr.grouping_id = g.id)") };
+1 -1
View File
@@ -105,7 +105,7 @@ namespace lms::db
}); });
} }
RangeResults<Image::pointer> Image::find(Session& session, const FindParameters& params) std::vector<Image::pointer> Image::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+8 -6
View File
@@ -22,6 +22,7 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -33,12 +34,14 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -112,7 +115,7 @@ namespace lms::db
} // namespace } // namespace
Language::Language(std::string_view name) 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 << "'"); 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<int>("SELECT COUNT(*) FROM language")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM language"));
} }
RangeResults<LanguageId> Language::findIds(Session& session, const FindParameters& params) std::vector<LanguageId> Language::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<LanguageId>(session, params) }; auto query{ createQuery<LanguageId>(session, params) };
return utils::execRangeQuery<LanguageId>(query, params.range); return utils::execRangeQuery<LanguageId>(query, params.range);
} }
RangeResults<Language::pointer> Language::find(Session& session, const FindParameters& params) std::vector<Language::pointer> Language::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) };
@@ -159,13 +162,12 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
if (name.size() > maxNameLength) name = core::stringUtils::utf8Truncate(name, maxNameLength);
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("name = ?").bind(name));
} }
RangeResults<LanguageId> Language::findOrphanIds(Session& session, std::optional<Range> range) std::vector<LanguageId> Language::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<LanguageId>("SELECT l.id FROM language l WHERE NOT EXISTS (SELECT 1 FROM track_language t_l WHERE t_l.language_id = l.id)") }; auto query{ session.getDboSession()->query<LanguageId>("SELECT l.id FROM language l WHERE NOT EXISTS (SELECT 1 FROM track_language t_l WHERE t_l.language_id = l.id)") };
+99 -33
View File
@@ -23,10 +23,12 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/Genre.hpp" #include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp" #include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp" #include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -46,10 +48,12 @@ namespace lms::db
auto query{ session.getDboSession()->query<ArtistId>("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") }; auto query{ session.getDboSession()->query<ArtistId>("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()) if (params.user.isValid())
query.where("l.user_id = ?").bind(params.user); {
query.join("user u ON u.id = l.user_id")
if (params.backend) .where("l.user_id = ?")
query.where("l.backend = ?").bind(*params.backend); .bind(params.user)
.where("l.backend = u.scrobbling_backend");
}
assert(!params.artist.isValid()); // poor check assert(!params.artist.isValid()); // poor check
@@ -123,7 +127,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const db::ClusterId id : params.filters.clusters) 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); query.bind(id);
} }
@@ -161,10 +165,12 @@ namespace lms::db
auto query{ session.getDboSession()->query<ReleaseId>("SELECT r.id from release r").join("track t ON t.release_id = r.id").join("listen l ON l.track_id = t.id") }; auto query{ session.getDboSession()->query<ReleaseId>("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()) if (params.user.isValid())
query.where("l.user_id = ?").bind(params.user); {
query.join("user u ON u.id = l.user_id")
if (params.backend) .where("l.user_id = ?")
query.where("l.backend = ?").bind(*params.backend); .bind(params.user)
.where("l.backend = u.scrobbling_backend");
}
if (params.artist.isValid()) if (params.artist.isValid())
{ {
@@ -223,7 +229,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId id : params.filters.clusters) for (const ClusterId id : params.filters.clusters)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause{ "c.id = ?" });
query.bind(id); query.bind(id);
} }
@@ -244,10 +250,12 @@ namespace lms::db
auto query{ session.getDboSession()->query<TrackId>("SELECT t.id from track t").join("listen l ON l.track_id = t.id") }; auto query{ session.getDboSession()->query<TrackId>("SELECT t.id from track t").join("listen l ON l.track_id = t.id") };
if (params.user.isValid()) if (params.user.isValid())
query.where("l.user_id = ?").bind(params.user); {
query.join("user u ON u.id = l.user_id")
if (params.backend) .where("l.user_id = ?")
query.where("l.backend = ?").bind(*params.backend); .bind(params.user)
.where("l.backend = u.scrobbling_backend");
}
if (params.artist.isValid()) if (params.artist.isValid())
{ {
@@ -305,7 +313,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId id : params.filters.clusters) 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); query.bind(id);
} }
@@ -348,7 +356,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l").where("l.id = ?").bind(id)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l").where("l.id = ?").bind(id));
} }
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters) std::vector<ListenId> Listen::find(Session& session, const FindParameters& parameters)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -373,39 +381,88 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>().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()))); return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>().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<ArtistId> Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params) std::vector<ArtistId> Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createArtistsQuery(session, params) }; auto query{ createArtistsQuery(session, params).orderBy("COUNT(a.id) DESC").groupBy("a.id") };
auto collection{ query
.orderBy("COUNT(a.id) DESC")
.groupBy("a.id") };
return utils::execRangeQuery<ArtistId>(query, params.range); return utils::execRangeQuery<ArtistId>(query, params.range);
} }
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, const StatsFindParameters& params) std::vector<ReleaseId> 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<ReleaseId>(query, params.range);
}
std::vector<TrackId> 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<TrackId>(query, params.range);
}
std::vector<ArtistId> 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<ArtistId>(query, params.range);
}
std::vector<ReleaseId> 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<ReleaseId>(query, params.range);
}
std::vector<TrackId> 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<TrackId>(query, params.range);
}
void Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params, const std::function<void(const ObjectPtr<Artist>&)>& 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<void(const ObjectPtr<Release>&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createReleasesQuery(session, params) auto query{ createReleasesQuery(session, params)
.orderBy("COUNT(r.id) DESC") .orderBy("COUNT(r.id) DESC")
.groupBy("r.id") }; .groupBy("r.id") };
return utils::execRangeQuery<ReleaseId>(query, params.range); utils::forEachQueryRangeResult(query, params.range, [&](const ReleaseId& id) {
if (const auto release{ Release::find(session, id) })
func(release);
});
} }
RangeResults<TrackId> Listen::getTopTracks(Session& session, const StatsFindParameters& params) void Listen::getTopTracks(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Track>&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createTracksQuery(session, params) auto query{ createTracksQuery(session, params)
.orderBy("COUNT(t.id) DESC") .orderBy("COUNT(t.id) DESC")
.groupBy("t.id") }; .groupBy("t.id") };
return utils::execRangeQuery<TrackId>(query, params.range); utils::forEachQueryRangeResult(query, params.range, [&](const TrackId& id) {
if (const auto track{ Track::find(session, id) })
func(track);
});
} }
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params) void Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params, const std::function<void(const ObjectPtr<Artist>&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createArtistsQuery(session, params) auto query{ createArtistsQuery(session, params)
@@ -413,10 +470,13 @@ namespace lms::db
.having("l.date_time = MAX(l.date_time)") .having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execRangeQuery<ArtistId>(query, params.range); utils::forEachQueryRangeResult(query, params.range, [&](const ArtistId& id) {
if (const auto artist{ Artist::find(session, id) })
func(artist);
});
} }
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, const StatsFindParameters& params) void Listen::getRecentReleases(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Release>&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createReleasesQuery(session, params) auto query{ createReleasesQuery(session, params)
@@ -424,10 +484,13 @@ namespace lms::db
.having("l.date_time = MAX(l.date_time)") .having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execRangeQuery<ReleaseId>(query, params.range); utils::forEachQueryRangeResult(query, params.range, [&](const ReleaseId& id) {
if (const auto release{ Release::find(session, id) })
func(release);
});
} }
RangeResults<TrackId> Listen::getRecentTracks(Session& session, const StatsFindParameters& params) void Listen::getRecentTracks(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Track>&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createTracksQuery(session, params) auto query{ createTracksQuery(session, params)
@@ -435,7 +498,10 @@ namespace lms::db
.having("l.date_time = MAX(l.date_time)") .having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execRangeQuery<TrackId>(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) std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId)
@@ -21,6 +21,8 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp" #include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp" #include "database/objects/Grouping.hpp"
@@ -38,7 +40,7 @@ DBO_INSTANTIATE_TEMPLATES(lms::db::MediaLibrary)
namespace lms::db namespace lms::db
{ {
MediaLibrary::MediaLibrary(std::string_view name, const std::filesystem::path& p) 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); setPath(p);
} }
+3 -1
View File
@@ -31,12 +31,14 @@
#include "database/objects/Language.hpp" #include "database/objects/Language.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -110,7 +112,7 @@ namespace lms::db
return IdRange<MediumId>{ .first = std::get<0>(res), .last = std::get<1>(res) }; return IdRange<MediumId>{ .first = std::get<0>(res), .last = std::get<1>(res) };
} }
RangeResults<MediumId> Medium::findOrphanIds(Session& session, std::optional<Range> range) std::vector<MediumId> Medium::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+8 -6
View File
@@ -22,6 +22,7 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -33,12 +34,14 @@
#include "database/objects/Language.hpp" #include "database/objects/Language.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
@@ -112,7 +115,7 @@ namespace lms::db
} // namespace } // namespace
Mood::Mood(std::string_view name) 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 << "'"); 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<int>("SELECT COUNT(*) FROM mood")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM mood"));
} }
RangeResults<MoodId> Mood::findIds(Session& session, const FindParameters& params) std::vector<MoodId> Mood::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<MoodId>(session, params) }; auto query{ createQuery<MoodId>(session, params) };
return utils::execRangeQuery<MoodId>(query, params.range); return utils::execRangeQuery<MoodId>(query, params.range);
} }
RangeResults<Mood::pointer> Mood::find(Session& session, const FindParameters& params) std::vector<Mood::pointer> Mood::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) };
@@ -159,13 +162,12 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
if (name.size() > maxNameLength) name = core::stringUtils::utf8Truncate(name, maxNameLength);
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("name = ?").bind(name));
} }
RangeResults<MoodId> Mood::findOrphanIds(Session& session, std::optional<Range> range) std::vector<MoodId> Mood::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<MoodId>("SELECT m.id FROM mood m WHERE NOT EXISTS (SELECT 1 FROM track_mood t_m WHERE t_m.mood_id = m.id)") }; auto query{ session.getDboSession()->query<MoodId>("SELECT m.id FROM mood m WHERE NOT EXISTS (SELECT 1 FROM track_mood t_m WHERE t_m.mood_id = m.id)") };
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "database/objects/Movement.hpp"
#include <Wt/Dbo/Impl.h>
#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<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& 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<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track)
{
return session.getDboSession()->add(std::unique_ptr<Movement>{ new Movement{ name, number, count, track } });
}
} // namespace lms::db
@@ -23,6 +23,8 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
@@ -171,7 +173,7 @@ namespace lms::db
void PlayListFile::setName(std::string_view name) 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 << "'"); LMS_LOG_IF(DB, WARNING, name.size() > _maxNameLength, "PlaylistFile name too long, truncated to '" << _name << "'");
} }
@@ -33,6 +33,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
@@ -40,6 +41,7 @@
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "database/objects/Work.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
+39 -16
View File
@@ -23,6 +23,8 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "core/PartialDateTime.hpp" #include "core/PartialDateTime.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
@@ -35,6 +37,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
@@ -42,6 +45,7 @@
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "database/objects/Work.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -93,6 +97,9 @@ namespace lms::db
query.join("track t ON t.release_id = r.id"); 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()) if (params.parentDirectory.isValid())
{ {
query.join("directory d ON t.directory_id = d.id"); query.join("directory d ON t.directory_id = d.id");
@@ -145,19 +152,35 @@ namespace lms::db
if (!params.name.empty()) if (!params.name.empty())
query.where("r.name = ?").bind(params.name); query.where("r.name = ?").bind(params.name);
for (std::string_view keyword : params.keywords) if (!params.keywords.empty())
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); {
std::vector<std::string> nameClauses;
std::vector<std::string> 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()) if (params.starringUser.isValid())
{ {
assert(params.feedbackBackend);
query.join("starred_release s_r ON s_r.release_id = r.id") 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 = ?") .where("s_r.user_id = ?")
.bind(params.starringUser) .bind(params.starringUser)
.where("s_r.backend = ?")
.bind(*params.feedbackBackend)
.where("s_r.sync_state <> ?") .where("s_r.sync_state <> ?")
.bind(SyncState::PendingRemove); .bind(SyncState::PendingRemove)
.where("s_r.backend = u.feedback_backend");
} }
if (params.artist.isValid()) if (params.artist.isValid())
@@ -213,7 +236,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId clusterId : params.filters.clusters) for (const ClusterId clusterId : params.filters.clusters)
{ {
clusterClause.Or(WhereClause("t_c.cluster_id = ?")); clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" });
query.bind(clusterId); query.bind(clusterId);
} }
@@ -356,7 +379,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Country>>("SELECT c from country c").where("c.name = ?").bind(name)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Country>>("SELECT c from country c").where("c.name = ?").bind(name));
} }
RangeResults<CountryId> Country::findOrphanIds(Session& session, std::optional<Range> range) std::vector<CountryId> Country::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -418,7 +441,7 @@ namespace lms::db
}); });
} }
RangeResults<LabelId> Label::findOrphanIds(Session& session, std::optional<Range> range) std::vector<LabelId> Label::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -480,7 +503,7 @@ namespace lms::db
}); });
} }
RangeResults<ReleaseTypeId> ReleaseType::findOrphanIds(Session& session, std::optional<Range> range) std::vector<ReleaseTypeId> ReleaseType::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -490,7 +513,7 @@ namespace lms::db
} }
Release::Release(const std::string& name, const std::optional<core::UUID>& MBID) Release::Release(const std::string& name, const std::optional<core::UUID>& MBID)
: _name{ std::string(name, 0, _maxNameLength) } : _name{ core::stringUtils::utf8Truncate(name, _maxNameLength) }
, _MBID{ MBID } , _MBID{ MBID }
{ {
} }
@@ -527,7 +550,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM release")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM release"));
} }
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range) std::vector<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -574,7 +597,7 @@ namespace lms::db
return IdRange<ReleaseId>{ .first = std::get<0>(res), .last = std::get<1>(res) }; return IdRange<ReleaseId>{ .first = std::get<0>(res), .last = std::get<1>(res) };
} }
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params) std::vector<Release::pointer> Release::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -590,7 +613,7 @@ namespace lms::db
utils::forEachQueryRangeResult(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params) std::vector<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); 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 "; 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; WhereClause clusterClause;
for (const ClusterTypeId clusterTypeId : clusterTypeIds) 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); where.And(clusterClause);
} }
oss << " " << where.get(); oss << " " << where.get();
@@ -22,6 +22,8 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Genre.hpp" #include "database/objects/Genre.hpp"
@@ -149,13 +151,13 @@ namespace lms::db
void ReleaseArtistLink::setArtistName(std::string_view artistName) 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 << "'"); LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'");
} }
void ReleaseArtistLink::setArtistSortName(std::string_view artistSortName) 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 << "'"); LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'");
} }
} // namespace lms::db } // namespace lms::db
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "database/objects/ServerInfo.hpp"
#include <Wt/Dbo/Impl.h>
#include <Wt/Dbo/WtSqlTraits.h>
#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<ServerInfo>()) };
if (!serverInfo)
return session.getDboSession()->add(std::unique_ptr<ServerInfo>{ new ServerInfo{ core::UUID::generate() } });
return serverInfo;
}
ServerInfo::pointer ServerInfo::get(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<ServerInfo>());
}
} // namespace lms::db
@@ -78,7 +78,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 from starred_track").where("track_id = ?").bind(trackId).where("user_id = ?").bind(userId).where("backend = ?").bind(backend)); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 from starred_track").where("track_id = ?").bind(trackId).where("user_id = ?").bind(userId).where("backend = ?").bind(backend));
} }
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params) std::vector<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+49 -21
View File
@@ -23,6 +23,7 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -36,12 +37,14 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp" #include "database/objects/TrackLyrics.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "database/objects/Work.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -77,14 +80,13 @@ namespace lms::db
if (params.starringUser.isValid()) if (params.starringUser.isValid())
{ {
assert(params.feedbackBackend);
query.join("starred_track s_t ON s_t.track_id = t.id") 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 = ?") .where("s_t.user_id = ?")
.bind(params.starringUser) .bind(params.starringUser)
.where("s_t.backend = ?")
.bind(*params.feedbackBackend)
.where("s_t.sync_state <> ?") .where("s_t.sync_state <> ?")
.bind(SyncState::PendingRemove); .bind(SyncState::PendingRemove)
.where("s_t.backend = u.feedback_backend");
} }
if (params.filters.clusters.size() == 1) if (params.filters.clusters.size() == 1)
@@ -103,7 +105,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId clusterId : params.filters.clusters) for (const ClusterId clusterId : params.filters.clusters)
{ {
clusterClause.Or(WhereClause("t_c.cluster_id = ?")); clusterClause.Or(WhereClause{ "t_c.cluster_id = ?" });
query.bind(clusterId); query.bind(clusterId);
} }
@@ -435,7 +437,7 @@ namespace lms::db
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.recording_mbid = ?").bind(mbid)); return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.recording_mbid = ?").bind(mbid));
} }
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range) std::vector<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -564,7 +566,7 @@ namespace lms::db
return _preferredMediaArtwork.id(); return _preferredMediaArtwork.id();
} }
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters) std::vector<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -572,7 +574,7 @@ namespace lms::db
return utils::execRangeQuery<TrackId>(query, parameters.range); return utils::execRangeQuery<TrackId>(query, parameters.range);
} }
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters) std::vector<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -588,14 +590,6 @@ namespace lms::db
utils::forEachQueryRangeResult(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
void Track::find(Session& session, const FindParameters& params, bool& moreResults, const std::function<void(const Track::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params) };
utils::forEachQueryRangeResult(query, params.range, moreResults, func);
}
std::size_t Track::getCount(Session& session, const FindParameters& params) std::size_t Track::getCount(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -621,19 +615,19 @@ namespace lms::db
void Track::setName(std::string_view name) 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 << "'"); LMS_LOG_IF(DB, WARNING, name.size() > _maxNameLength, "Track name too long, truncated to '" << _name << "'");
} }
void Track::setCopyright(std::string_view copyright) 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 << "'"); LMS_LOG_IF(DB, WARNING, copyright.size() > _maxCopyrightLength, "Track copyright too long, truncated to '" << _copyright << "'");
} }
void Track::setCopyrightURL(std::string_view copyrightURL) 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 << "'"); 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)); _moods.insert(getDboPtr(mood));
} }
void Track::setWorks(std::span<const ObjectPtr<Work>> works)
{
_works.clear();
for (const ObjectPtr<Work>& work : works)
_works.insert(getDboPtr(work));
}
std::vector<Work::pointer> Track::getWorks() const
{
// deterministic order, callers rely on the first entry
return utils::fetchQueryResults<Work::pointer>(_works.find().orderBy("id"));
}
bool Track::hasWork() const
{
return !_works.empty();
}
void Track::clearMovements()
{
_movements.clear();
}
std::vector<Movement::pointer> Track::getMovements() const
{
// deterministic order, callers rely on the first entry
return utils::fetchQueryResults<Movement::pointer>(_movements.find().orderBy("id"));
}
bool Track::hasMovement() const
{
return !_movements.empty();
}
void Track::clearLyrics() void Track::clearLyrics()
{ {
_trackLyrics.clear(); _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"; 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; WhereClause clusterClause;
for (ClusterTypeId clusterTypeId : clusterTypeIds) 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); where.And(clusterClause);
} }
oss << " " << where.get(); oss << " " << where.get();
@@ -22,6 +22,8 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Genre.hpp" #include "database/objects/Genre.hpp"
@@ -191,13 +193,13 @@ namespace lms::db
void TrackArtistLink::setArtistName(std::string_view artistName) 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 << "'"); LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'");
} }
void TrackArtistLink::setArtistSortName(std::string_view artistSortName) 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 << "'"); LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'");
} }
} // namespace lms::db } // namespace lms::db
@@ -54,7 +54,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_bookmark")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_bookmark"));
} }
RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range) std::vector<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -27,7 +27,9 @@
#include "database/objects/Grouping.hpp" #include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp" #include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/Work.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
@@ -150,7 +152,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(query); return utils::fetchQuerySingleResult(query);
} }
RangeResults<TrackEmbeddedImageId> TrackEmbeddedImage::findOrphanIds(Session& session, std::optional<Range> range) std::vector<TrackEmbeddedImageId> TrackEmbeddedImage::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+7 -5
View File
@@ -29,9 +29,11 @@
#include "database/objects/Grouping.hpp" #include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp" #include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp" #include "database/objects/Mood.hpp"
#include "database/objects/Movement.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "database/objects/Work.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -113,7 +115,7 @@ namespace lms::db
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId clusterId : params.filters.clusters) for (const ClusterId clusterId : params.filters.clusters)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause{ "c.id = ?" });
query.bind(clusterId); query.bind(clusterId);
} }
@@ -182,7 +184,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackList>>("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)); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackList>>("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<TrackListId> TrackList::find(Session& session, const FindParameters& params) std::vector<TrackListId> TrackList::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<TrackListId>(session, params) }; auto query{ createQuery<TrackListId>(session, params) };
@@ -218,13 +220,13 @@ namespace lms::db
TrackListEntry::pointer res; TrackListEntry::pointer res;
auto entries = getEntries(Range{ pos, 1 }); auto entries = getEntries(Range{ pos, 1 });
if (!entries.results.empty()) if (!entries.empty())
res = entries.results.front(); res = entries.front();
return res; return res;
} }
RangeResults<ObjectPtr<TrackListEntry>> TrackList::getEntries(std::optional<Range> range) const std::vector<ObjectPtr<TrackListEntry>> TrackList::getEntries(std::optional<Range> range) const
{ {
assert(session()); assert(session());
@@ -121,7 +121,7 @@ namespace lms::db
}); });
} }
RangeResults<TrackLyricsId> TrackLyrics::findOrphanIds(Session& session, std::optional<Range> range) std::vector<TrackLyricsId> TrackLyrics::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -66,7 +66,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("track_id = ?").bind(trackId)); return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackMusicNNEmbeddings>().where("track_id = ?").bind(trackId));
} }
RangeResults<TrackMusicNNEmbeddingsId> TrackMusicNNEmbeddings::find(Session& session, std::optional<Range> range) std::vector<TrackMusicNNEmbeddingsId> TrackMusicNNEmbeddings::find(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+1 -1
View File
@@ -61,7 +61,7 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM user")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM user"));
} }
RangeResults<UserId> User::find(Session& session, const FindParameters& params) std::vector<UserId> User::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
+112
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "database/objects/Work.hpp"
#include <Wt/Dbo/Impl.h>
#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<core::UUID>& 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<core::UUID>& mbid)
{
return session.getDboSession()->add(std::unique_ptr<Work>{ new Work{ name, mbid } });
}
Work::pointer Work::find(Session& session, WorkId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Work>().where("id = ?").bind(id));
}
Work::pointer Work::find(Session& session, const core::UUID& mbid)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Work>().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<Wt::Dbo::ptr<Work>>("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<WorkId> Work::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<WorkId>("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<WorkId>(query, range);
}
} // namespace lms::db
@@ -28,6 +28,7 @@
#include "database/Transaction.hpp" #include "database/Transaction.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Types.hpp"
namespace lms::db namespace lms::db
{ {
@@ -58,6 +59,8 @@ namespace lms::db
void prepareTablesIfNeeded(); // need to run only once at startup void prepareTablesIfNeeded(); // need to run only once at startup
bool migrateSchemaIfNeeded(); // returns true if migration was performed bool migrateSchemaIfNeeded(); // returns true if migration was performed
void createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType = RecommendationEngineType::Clusters);
void createServerInfoIfNeeded();
void createIndexesIfNeeded(); void createIndexesIfNeeded();
void vacuumIfNeeded(); void vacuumIfNeeded();
void vacuum(); void vacuum();
@@ -38,8 +38,6 @@ namespace lms::db
// Request: // Request:
// size = 0 => means we don't want data // size = 0 => means we don't want data
// Response (via RangeResults)
// size => results size
struct Range struct Range
{ {
std::size_t offset{}; std::size_t offset{};
@@ -65,40 +63,6 @@ namespace lms::db
} }
} }
template<typename T>
struct RangeResults
{
Range range;
std::vector<T> 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 struct FileStats
{ {
std::size_t trackCount; std::size_t trackCount;
@@ -66,9 +66,8 @@ namespace lms::db
ArtistSortMethod sortMethod{ ArtistSortMethod::None }; ArtistSortMethod sortMethod{ ArtistSortMethod::None };
std::optional<Range> range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user UserId starringUser; // only artists starred by this user (uses their current feedback backend)
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend TrackId track; // artists involved in this track
TrackId track; // artists involved in this track
FindParameters& setFilters(const Filters& _filters) FindParameters& setFilters(const Filters& _filters)
{ {
@@ -105,10 +104,9 @@ namespace lms::db
writtenAfter = _after; writtenAfter = _after;
return *this; return *this;
} }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) FindParameters& setStarringUser(UserId _user)
{ {
starringUser = _user; starringUser = _user;
feedbackBackend = _feedbackBackend;
return *this; return *this;
} }
FindParameters& setTrack(TrackId _track) FindParameters& setTrack(TrackId _track)
@@ -127,13 +125,13 @@ namespace lms::db
static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field static std::vector<pointer> 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<void(const Artist::pointer&)>& func, MediaLibraryId library = {}); static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function<void(const Artist::pointer&)>& func, MediaLibraryId library = {});
static void find(Session& session, const IdRange<ArtistId>& idRange, const std::function<void(const Artist::pointer&)>& func); static void find(Session& session, const IdRange<ArtistId>& idRange, const std::function<void(const Artist::pointer&)>& func);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static IdRange<ArtistId> findNextIdRange(Session& session, ArtistId lastRetrievedId, std::size_t count); static IdRange<ArtistId> findNextIdRange(Session& session, ArtistId lastRetrievedId, std::size_t count);
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& params); static std::vector<ArtistId> findIds(Session& session, const FindParameters& params);
static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related static std::vector<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
static bool exists(Session& session, ArtistId id); static bool exists(Session& session, ArtistId id);
static RangeResults<pointer> findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range = std::nullopt); static std::vector<pointer> findWithMBIDNameVariants(Session& session, ArtistId& lastRetrievedArtist, std::optional<Range> range = std::nullopt);
// Updates // Updates
static void updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId); static void updatePreferredArtwork(Session& session, ArtistId artistId, ArtworkId artworkId);
@@ -91,11 +91,11 @@ namespace lms::db
// Find utility // Find utility
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterId> findIds(Session& session, const FindParameters& params); static std::vector<ClusterId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func);
static pointer find(Session& session, ClusterId id); static pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<ClusterId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// May be very slow // May be very slow
static std::size_t computeTrackCount(Session& session, ClusterId id); static std::size_t computeTrackCount(Session& session, ClusterId id);
@@ -104,7 +104,7 @@ namespace lms::db
// Accessors // Accessors
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; } ObjectPtr<ClusterType> getType() const { return _clusterType; }
RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const; std::vector<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
void addTrack(ObjectPtr<Track> track); void addTrack(ObjectPtr<Track> track);
template<class Action> template<class Action>
@@ -136,12 +136,12 @@ namespace lms::db
// Getters // Getters
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
static void find(Session& session, const std::function<void(const pointer&)>& func); static void find(Session& session, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static pointer find(Session& session, ClusterTypeId id); static pointer find(Session& session, ClusterTypeId id);
static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt); static std::vector<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
static void remove(Session& session, const std::string& name); static void remove(Session& session, const std::string& name);
@@ -122,11 +122,11 @@ namespace lms::db
static pointer find(Session& session, DirectoryId id); static pointer find(Session& session, DirectoryId id);
static pointer find(Session& session, const std::filesystem::path& path); static pointer find(Session& session, const std::filesystem::path& path);
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<Directory::pointer> find(Session& session, const FindParameters& params); static std::vector<Directory::pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId); static std::vector<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId);
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt); static std::vector<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
// getters // getters
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; } const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
@@ -82,12 +82,12 @@ namespace lms::db
Genre() = default; Genre() = default;
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<GenreId> findIds(Session& session, const FindParameters& params); static std::vector<GenreId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static pointer find(Session& session, GenreId id); static pointer find(Session& session, GenreId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static RangeResults<GenreId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<GenreId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static std::size_t computeTrackCount(Session& session, GenreId id); static std::size_t computeTrackCount(Session& session, GenreId id);
static std::size_t computeReleaseCount(Session& session, GenreId id); static std::size_t computeReleaseCount(Session& session, GenreId id);
@@ -82,12 +82,12 @@ namespace lms::db
Grouping() = default; Grouping() = default;
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<GroupingId> findIds(Session& session, const FindParameters& params); static std::vector<GroupingId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static pointer find(Session& session, GroupingId id); static pointer find(Session& session, GroupingId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static RangeResults<GroupingId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<GroupingId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
@@ -73,7 +73,7 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, ImageId id); static pointer find(Session& session, ImageId id);
static pointer find(Session& session, const std::filesystem::path& file); static pointer find(Session& session, const std::filesystem::path& file);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Image::pointer&)>& func); static void find(Session& session, const FindParameters& parameters, const std::function<void(const Image::pointer&)>& func);
static void find(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function<void(const Image::pointer&)>& func); static void find(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function<void(const Image::pointer&)>& func);
static void findAbsoluteFilePath(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function<void(ImageId imageId, const std::filesystem::path& absoluteFilePath)>& func); static void findAbsoluteFilePath(Session& session, ImageId& lastRetrievedId, std::size_t count, const std::function<void(ImageId imageId, const std::filesystem::path& absoluteFilePath)>& func);
@@ -82,12 +82,12 @@ namespace lms::db
Language() = default; Language() = default;
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<LanguageId> findIds(Session& session, const FindParameters& params); static std::vector<LanguageId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static pointer find(Session& session, LanguageId id); static pointer find(Session& session, LanguageId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static RangeResults<LanguageId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<LanguageId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <functional>
#include <optional> #include <optional>
#include <Wt/Dbo/Field.h> #include <Wt/Dbo/Field.h>
@@ -36,6 +37,8 @@
namespace lms::db namespace lms::db
{ {
class Artist;
class Release;
class Session; class Session;
class Track; class Track;
class User; class User;
@@ -78,13 +81,12 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, ListenId id); static pointer find(Session& session, ListenId id);
static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime); static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters); static std::vector<ListenId> find(Session& session, const FindParameters& parameters);
// Stats // Stats
struct StatsFindParameters struct StatsFindParameters
{ {
UserId user; UserId user;
std::optional<ScrobblingBackend> backend;
db::Filters filters; db::Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<Range> range; std::optional<Range> range;
@@ -95,11 +97,6 @@ namespace lms::db
user = _user; user = _user;
return *this; return *this;
} }
StatsFindParameters& setScrobblingBackend(std::optional<ScrobblingBackend> _backend)
{
backend = _backend;
return *this;
}
StatsFindParameters& setFilters(const db::Filters& _filters) StatsFindParameters& setFilters(const db::Filters& _filters)
{ {
filters = _filters; filters = _filters;
@@ -139,13 +136,19 @@ namespace lms::db
} }
}; };
static RangeResults<ArtistId> getTopArtists(Session& session, const ArtistStatsFindParameters& params); static std::vector<ArtistId> getTopArtists(Session& session, const ArtistStatsFindParameters& params);
static RangeResults<ReleaseId> getTopReleases(Session& session, const StatsFindParameters& params); static std::vector<ReleaseId> getTopReleases(Session& session, const StatsFindParameters& params);
static RangeResults<TrackId> getTopTracks(Session& session, const StatsFindParameters& params); static std::vector<TrackId> getTopTracks(Session& session, const StatsFindParameters& params);
static std::vector<ArtistId> getRecentArtists(Session& session, const ArtistStatsFindParameters& params);
static std::vector<ReleaseId> getRecentReleases(Session& session, const StatsFindParameters& params);
static std::vector<TrackId> getRecentTracks(Session& session, const StatsFindParameters& params);
static RangeResults<ArtistId> getRecentArtists(Session& session, const ArtistStatsFindParameters& params); static void getTopArtists(Session& session, const ArtistStatsFindParameters& params, const std::function<void(const ObjectPtr<Artist>&)>& func);
static RangeResults<ReleaseId> getRecentReleases(Session& session, const StatsFindParameters& params); static void getTopReleases(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Release>&)>& func);
static RangeResults<TrackId> getRecentTracks(Session& session, const StatsFindParameters& params); static void getTopTracks(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Track>&)>& func);
static void getRecentArtists(Session& session, const ArtistStatsFindParameters& params, const std::function<void(const ObjectPtr<Artist>&)>& func);
static void getRecentReleases(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Release>&)>& func);
static void getRecentTracks(Session& session, const StatsFindParameters& params, const std::function<void(const ObjectPtr<Track>&)>& 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, TrackId trackId); // for the current backend
static std::size_t getCount(Session& session, UserId userId, ReleaseId trackId); // for the current backend static std::size_t getCount(Session& session, UserId userId, ReleaseId trackId); // for the current backend
@@ -78,7 +78,7 @@ namespace lms::db
static pointer find(Session& session, ReleaseId id, std::optional<std::size_t> position); static pointer find(Session& session, ReleaseId id, std::optional<std::size_t> position);
static void find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func); static void find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func);
static IdRange<MediumId> findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count); static IdRange<MediumId> findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count);
static RangeResults<MediumId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<MediumId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Updates // Updates
static void updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId); static void updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId);
@@ -82,12 +82,12 @@ namespace lms::db
Mood() = default; Mood() = default;
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<MoodId> findIds(Session& session, const FindParameters& params); static std::vector<MoodId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static pointer find(Session& session, MoodId id); static pointer find(Session& session, MoodId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static RangeResults<MoodId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<MoodId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Field.h>
#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<Movement, MovementId>
{
public:
static constexpr std::size_t maxNameLength{ 512 };
Movement() = default;
static pointer create(Session& session, std::string_view name, std::optional<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track);
std::string_view getName() const { return _name; }
std::optional<std::size_t> getNumber() const { return _number; }
std::optional<std::size_t> getCount() const { return _count; }
template<class Action>
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<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track);
std::string _name;
std::optional<int> _number;
std::optional<int> _count;
Wt::Dbo::ptr<Track> _track;
};
} // namespace lms::db
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(MovementId)
@@ -70,7 +70,7 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, CountryId id); static pointer find(Session& session, CountryId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static RangeResults<CountryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<CountryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors // Accessors
std::string_view getName() const { return _name; } 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, LabelId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static void find(Session& session, LabelSortMethod sortMethod, std::function<void(const Label::pointer& label)> func); static void find(Session& session, LabelSortMethod sortMethod, std::function<void(const Label::pointer& label)> func);
static RangeResults<LabelId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<LabelId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors // Accessors
std::string_view getName() const { return _name; } 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, ReleaseTypeId id);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static void find(Session& session, ReleaseTypeSortMethod sortMethod, std::function<void(const ReleaseType::pointer& releaseType)> func); static void find(Session& session, ReleaseTypeSortMethod sortMethod, std::function<void(const ReleaseType::pointer& releaseType)> func);
static RangeResults<ReleaseTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static std::vector<ReleaseTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors // Accessors
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
@@ -170,8 +170,7 @@ namespace lms::db
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
std::optional<YearRange> dateRange; std::optional<YearRange> dateRange;
std::optional<YearRange> originalDateRange; std::optional<YearRange> originalDateRange;
UserId starringUser; // only releases starred by this user UserId starringUser; // only releases starred by this user (uses their current feedback backend)
std::optional<FeedbackBackend> feedbackBackend; // and for this backend
ArtistId artist; // only releases by this release artist ArtistId artist; // only releases by this release artist
ArtistId trackArtist; // only releases that involved this track artist ArtistId trackArtist; // only releases that involved this track artist
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types, if set core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types, if set
@@ -220,10 +219,9 @@ namespace lms::db
originalDateRange = _originalDateRange; originalDateRange = _originalDateRange;
return *this; return *this;
} }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) FindParameters& setStarringUser(UserId _user)
{ {
starringUser = _user; starringUser = _user;
feedbackBackend = _feedbackBackend;
return *this; return *this;
} }
FindParameters& setArtist(ArtistId _artist) 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<void(const Release::pointer&)>& func, MediaLibraryId library = {}); static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function<void(const Release::pointer&)>& func, MediaLibraryId library = {});
static void find(Session& session, const IdRange<ReleaseId>& idRange, const std::function<void(const Release::pointer&)>& func); static void find(Session& session, const IdRange<ReleaseId>& idRange, const std::function<void(const Release::pointer&)>& func);
static IdRange<ReleaseId> findNextIdRange(Session& session, ReleaseId lastRetrievedId, std::size_t count); static IdRange<ReleaseId> findNextIdRange(Session& session, ReleaseId lastRetrievedId, std::size_t count);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static std::vector<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func); static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func);
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters); static std::vector<ReleaseId> findIds(Session& session, const FindParameters& parameters);
static std::size_t getCount(Session& session, const FindParameters& parameters); static std::size_t getCount(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related static std::vector<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related
// Updates // Updates
static void updatePreferredArtwork(Session& session, ReleaseId id, ArtworkId artworkId); static void updatePreferredArtwork(Session& session, ReleaseId id, ArtworkId artworkId);
@@ -29,6 +29,7 @@
#include "database/IdType.hpp" #include "database/IdType.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/objects/Types.hpp"
LMS_DECLARE_IDTYPE(ScanSettingsId) LMS_DECLARE_IDTYPE(ScanSettingsId)
@@ -49,14 +50,6 @@ namespace lms::db
Hourly, Hourly,
}; };
// Do not modify values (just add)
enum class RecommendationEngineType
{
Clusters = 0,
None = 2,
AudioSimilarity = 3,
};
ScanSettings() = default; ScanSettings() = default;
static pointer find(Session& session, std::string_view name = ""); static pointer find(Session& session, std::string_view name = "");
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/Field.h>
#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>;
ServerInfo() = default;
static pointer getOrCreate(Session& session);
static pointer get(Session& session);
core::UUID getInstanceId() const { return _instanceId; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _instanceId, "instance_id");
}
private:
explicit ServerInfo(core::UUID instanceId);
core::UUID _instanceId;
};
} // namespace lms::db
@@ -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); // current feedback backend
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend); static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
static bool exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend); static bool exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams); static std::vector<StarredTrackId> find(Session& session, const FindParameters& findParams);
// Accessors // Accessors
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
@@ -51,12 +51,14 @@
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MediumId.hpp" #include "database/objects/MediumId.hpp"
#include "database/objects/MoodId.hpp" #include "database/objects/MoodId.hpp"
#include "database/objects/MovementId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
#include "database/objects/TrackListId.hpp" #include "database/objects/TrackListId.hpp"
#include "database/objects/Types.hpp" #include "database/objects/Types.hpp"
#include "database/objects/UserId.hpp" #include "database/objects/UserId.hpp"
#include "database/objects/WorkId.hpp"
#include "database/objects/detail/Types.hpp" #include "database/objects/detail/Types.hpp"
namespace lms::db namespace lms::db
@@ -69,16 +71,18 @@ namespace lms::db
class Genre; class Genre;
class Grouping; class Grouping;
class Language; class Language;
class Medium;
class Movement;
class Mood; class Mood;
class TrackEmbeddedImageLink; class TrackEmbeddedImageLink;
class MediaLibrary; class MediaLibrary;
class Medium;
class Release; class Release;
class Session; class Session;
class TrackArtistLink; class TrackArtistLink;
class TrackLyrics; class TrackLyrics;
class TrackStats; class TrackStats;
class User; class User;
class Work;
class Track final : public Object<Track, TrackId> class Track final : public Object<Track, TrackId>
{ {
@@ -91,8 +95,7 @@ namespace lms::db
TrackSortMethod sortMethod{ TrackSortMethod::None }; TrackSortMethod sortMethod{ TrackSortMethod::None };
std::optional<Range> range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user UserId starringUser; // only tracks starred by this user (uses their current feedback backend)
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
ArtistId artist; // only tracks that involve this artist ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name std::string artistName; // only tracks that involve this artist name
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
@@ -138,10 +141,9 @@ namespace lms::db
writtenAfter = _after; writtenAfter = _after;
return *this; return *this;
} }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) FindParameters& setStarringUser(UserId _user)
{ {
starringUser = _user; starringUser = _user;
feedbackBackend = _feedbackBackend;
return *this; return *this;
} }
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
@@ -231,12 +233,11 @@ namespace lms::db
static bool exists(Session& session, TrackId id); static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID); static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID); static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID);
static RangeResults<TrackId> findIds(Session& session, const FindParameters& params); static std::vector<TrackId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params); static std::vector<pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, const std::function<void(const Track::pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const Track::pointer&)>& func);
static void find(Session& session, const FindParameters& params, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
static std::size_t getCount(Session& session, const FindParameters& params); static std::size_t getCount(Session& session, const FindParameters& params);
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt); static std::vector<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
// Update utility functions // Update utility functions
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId); static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
@@ -282,6 +283,8 @@ namespace lms::db
void setGroupings(std::span<const ObjectPtr<Grouping>> groupings); void setGroupings(std::span<const ObjectPtr<Grouping>> groupings);
void setLanguages(std::span<const ObjectPtr<Language>> languages); void setLanguages(std::span<const ObjectPtr<Language>> languages);
void setMoods(std::span<const ObjectPtr<Mood>> moods); void setMoods(std::span<const ObjectPtr<Mood>> moods);
void setWorks(std::span<const ObjectPtr<Work>> works);
void clearMovements();
void clearLyrics(); void clearLyrics();
void clearEmbeddedLyrics(); void clearEmbeddedLyrics();
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics); void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
@@ -313,7 +316,7 @@ namespace lms::db
// Metadata // Metadata
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; } std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::string getName() const { return _name; } std::string_view getName() const { return _name; }
const core::PartialDateTime& getDate() const { return _date; } const core::PartialDateTime& getDate() const { return _date; }
std::optional<int> getYear() const; std::optional<int> getYear() const;
const core::PartialDateTime& getOriginalDate() const { return _originalDate; } const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
@@ -350,6 +353,10 @@ namespace lms::db
std::vector<LanguageId> getLanguageIds() const; std::vector<LanguageId> getLanguageIds() const;
std::vector<ObjectPtr<Mood>> getMoods() const; std::vector<ObjectPtr<Mood>> getMoods() const;
std::vector<MoodId> getMoodIds() const; std::vector<MoodId> getMoodIds() const;
std::vector<ObjectPtr<Work>> getWorks() const;
bool hasWork() const;
std::vector<ObjectPtr<Movement>> getMovements() const;
bool hasMovement() const;
ObjectPtr<MediaLibrary> getMediaLibrary() const; ObjectPtr<MediaLibrary> getMediaLibrary() const;
ObjectPtr<Directory> getDirectory() const; ObjectPtr<Directory> getDirectory() const;
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
@@ -396,12 +403,14 @@ namespace lms::db
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_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, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); 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, _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, _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, _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, _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, _trackLyrics, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _embeddedImageLinks, 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 _artistDisplayName;
std::string _comment; std::string _comment;
Advisory _advisory{ Advisory::UnSet }; Advisory _advisory{ Advisory::UnSet };
Wt::Dbo::ptr<Medium> _medium; Wt::Dbo::ptr<Medium> _medium;
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary; Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
@@ -457,6 +465,8 @@ namespace lms::db
Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings; Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings;
Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages; Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages;
Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods; Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods;
Wt::Dbo::collection<Wt::Dbo::ptr<Work>> _works;
Wt::Dbo::collection<Wt::Dbo::ptr<Movement>> _movements;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics; Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
}; };
@@ -46,7 +46,7 @@ namespace lms::db
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackBookmarkId id); static pointer find(Session& session, TrackBookmarkId id);
static RangeResults<TrackBookmarkId> find(Session& session, UserId userId, std::optional<Range> range = std::nullopt); static std::vector<TrackBookmarkId> find(Session& session, UserId userId, std::optional<Range> range = std::nullopt);
static pointer find(Session& session, UserId userId, TrackId trackId); static pointer find(Session& session, UserId userId, TrackId trackId);
// Setters // Setters
@@ -100,7 +100,7 @@ namespace lms::db
static pointer find(Session& session, TrackEmbeddedImageId id); static pointer find(Session& session, TrackEmbeddedImageId id);
static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, std::size_t size, ImageHashType hash); static pointer find(Session& session, std::size_t size, ImageHashType hash);
static RangeResults<TrackEmbeddedImageId> findOrphanIds(Session& session, std::optional<Range> range); static std::vector<TrackEmbeddedImageId> findOrphanIds(Session& session, std::optional<Range> range);
// getters // getters
ImageHashType getHash() const { return _hash; } ImageHashType getHash() const { return _hash; }
@@ -116,7 +116,7 @@ namespace lms::db
static std::size_t getCount(Session& session); 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, std::string_view name, TrackListType type, UserId userId);
static pointer find(Session& session, TrackListId tracklistId); static pointer find(Session& session, TrackListId tracklistId);
static RangeResults<TrackListId> find(Session& session, const FindParameters& params); static std::vector<TrackListId> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, const std::function<void(const TrackList::pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const TrackList::pointer&)>& func);
// Accessors // Accessors
@@ -139,7 +139,7 @@ namespace lms::db
bool isEmpty() const; bool isEmpty() const;
std::size_t getCount() const; std::size_t getCount() const;
ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const; ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const;
RangeResults<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const; std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const; ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
std::vector<TrackId> getTrackIds() const; std::vector<TrackId> getTrackIds() const;
@@ -83,7 +83,7 @@ namespace lms::db
static pointer find(Session& session, const std::filesystem::path& file); static pointer find(Session& session, const std::filesystem::path& file);
static void find(Session& session, const FindParameters& params, const std::function<void(const TrackLyrics::pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const TrackLyrics::pointer&)>& func);
static void find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(const TrackLyrics::pointer&)>& func); static void find(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(const TrackLyrics::pointer&)>& func);
static RangeResults<TrackLyricsId> findOrphanIds(Session& session, std::optional<Range> range); static std::vector<TrackLyricsId> findOrphanIds(Session& session, std::optional<Range> range);
static void findAbsoluteFilePath(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(TrackLyricsId trackLyricsId, const std::filesystem::path& absoluteFilePath)>& func); static void findAbsoluteFilePath(Session& session, TrackLyricsId& lastRetrievedId, std::size_t count, const std::function<void(TrackLyricsId trackLyricsId, const std::filesystem::path& absoluteFilePath)>& func);
using SynchronizedLines = std::map<std::chrono::milliseconds, std::string>; using SynchronizedLines = std::map<std::chrono::milliseconds, std::string>;
@@ -45,7 +45,7 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackMusicNNEmbeddingsId id); static pointer find(Session& session, TrackMusicNNEmbeddingsId id);
static pointer find(Session& session, TrackId trackId); static pointer find(Session& session, TrackId trackId);
static RangeResults<TrackMusicNNEmbeddingsId> find(Session& session, std::optional<Range> range = std::nullopt); static std::vector<TrackMusicNNEmbeddingsId> find(Session& session, std::optional<Range> range = std::nullopt);
static void find(Session& session, std::function<void(const pointer&)> func); static void find(Session& session, std::function<void(const pointer&)> func);
static void removeAll(Session& session); static void removeAll(Session& session);
@@ -114,6 +114,13 @@ namespace lms::db
PubDateDesc, PubDateDesc,
}; };
enum class RecommendationEngineType
{
Clusters = 0,
None = 2,
AudioSimilarity = 3,
};
enum class ReleaseArtistLinkSortMethod enum class ReleaseArtistLinkSortMethod
{ {
None, None,
@@ -88,7 +88,7 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, UserId id); static pointer find(Session& session, UserId id);
static pointer find(Session& session, std::string_view loginName); static pointer find(Session& session, std::string_view loginName);
static RangeResults<UserId> find(Session& session, const FindParameters& params); static std::vector<UserId> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
static pointer findDemoUser(Session& session); static pointer findDemoUser(Session& session);
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h>
#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<Work, WorkId>
{
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<WorkId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
void setName(std::string_view name);
std::string_view getName() const { return _name; }
std::optional<core::UUID> getMBID() const { return _mbid; }
template<class Action>
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<core::UUID>& mbid);
static pointer create(Session& session, std::string_view name, const std::optional<core::UUID>& mbid);
std::string _name;
std::optional<core::UUID> _mbid;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
};
} // namespace lms::db
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(WorkId)
+107 -122
View File
@@ -55,20 +55,20 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
artists = Artist::findOrphanIds(session); artists = Artist::findOrphanIds(session);
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::find(session, Artist::FindParameters{}) }; auto artists{ Artist::find(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
} }
{ {
@@ -181,10 +181,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 0); ASSERT_EQ(artists.size(), 0);
ASSERT_FALSE(artists.moreResults);
ASSERT_EQ(artists.range.offset, 0);
ASSERT_EQ(artists.range.size, 0);
} }
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
@@ -195,43 +192,31 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 3); ASSERT_EQ(artists.size(), 3);
ASSERT_FALSE(artists.moreResults);
ASSERT_EQ(artists.range.offset, 0);
ASSERT_EQ(artists.range.size, 3);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 0, 1 })) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 0, 1 })) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
ASSERT_TRUE(artists.moreResults); EXPECT_EQ(artists[0], artist1.getId());
ASSERT_EQ(artists.range.offset, 0);
ASSERT_EQ(artists.range.size, 1);
EXPECT_EQ(artists.results[0], artist1.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 1, 1 })) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 1, 1 })) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
ASSERT_TRUE(artists.moreResults); EXPECT_EQ(artists[0], artist2.getId());
ASSERT_EQ(artists.range.offset, 1);
ASSERT_EQ(artists.range.size, 1);
EXPECT_EQ(artists.results[0], artist2.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 2, 1 })) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRange(Range{ 2, 1 })) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
ASSERT_FALSE(artists.moreResults); EXPECT_EQ(artists[0], artist3.getId());
ASSERT_EQ(artists.range.offset, 2);
ASSERT_EQ(artists.range.size, 1);
EXPECT_EQ(artists.results[0], artist3.getId());
} }
} }
@@ -249,7 +234,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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 transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) }; 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 transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) }; 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 transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; 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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::findOrphanIds(session).results, std::vector<db::ArtistId>{ artist.getId() }); EXPECT_EQ(Artist::findOrphanIds(session), std::vector<db::ArtistId>{ artist.getId() });
} }
{ {
@@ -353,7 +338,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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 transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; 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 transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library1->getId()))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library1->getId()))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library2->getId()))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library2->getId()))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(otherLibrary->getId()))) }; 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 transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setMediaLibrary(library->getId()))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
@@ -479,8 +464,8 @@ namespace lms::db::tests
params.setFilters(Filters{}.setMediaLibrary(library.getId())); params.setFilters(Filters{}.setMediaLibrary(library.getId()));
auto artists{ Artist::findIds(session, params) }; auto artists{ Artist::findIds(session, params) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
{ {
@@ -491,7 +476,7 @@ namespace lms::db::tests
params.setFilters(Filters{}.setMediaLibrary(library.getId())); params.setFilters(Filters{}.setMediaLibrary(library.getId()));
auto artists{ Artist::findIds(session, params) }; 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())); params.setFilters(Filters{}.setMediaLibrary(library.getId()));
auto artists{ Artist::findIds(session, params) }; auto artists{ Artist::findIds(session, params) };
EXPECT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
} }
@@ -521,18 +506,18 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Composer)).results.size(), 0); 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); EXPECT_EQ(track->getArtistLinks().size(), 3);
auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId())) }; 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 })); 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 })); 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 })); 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 })); 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() }; 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({}).size(), 2);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Arranger }).size(), 0); 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{}).size(), 2);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).size(), 2);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId())) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.front(), track->getId());
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId())); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId()));
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.front(), track->getId());
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Arranger })); 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 })); 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 })); 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 })); 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() }; 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() }; 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)); params.setFilters(Filters{}.setCodec(core::media::Codec::FLAC));
const auto artists{ Artist::find(session, params) }; const auto artists{ Artist::find(session, params) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist2.getId()); EXPECT_EQ(artists.front()->getId(), artist2.getId());
} }
} }
@@ -753,15 +738,15 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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" })) }; const auto artistsByAAA{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "A" })) };
ASSERT_EQ(artistsByAAA.results.size(), 1); ASSERT_EQ(artistsByAAA.size(), 1);
EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); EXPECT_EQ(artistsByAAA.front(), artist.getId());
const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "Z" })) }; const auto artistsByZZZ{ Artist::Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "Z" })) };
ASSERT_EQ(artistsByZZZ.results.size(), 1); ASSERT_EQ(artistsByZZZ.size(), 1);
EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); EXPECT_EQ(artistsByZZZ.front(), artist.getId());
EXPECT_EQ(Artist::find(session, "NNN").size(), 0); EXPECT_EQ(Artist::find(session, "NNN").size(), 0);
EXPECT_EQ(Artist::find(session, "AAA").size(), 1); EXPECT_EQ(Artist::find(session, "AAA").size(), 1);
@@ -804,27 +789,27 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
{ {
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "MyArtist" })) }; 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)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "MyArtist%" }).setSortMethod(ArtistSortMethod::Name)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId()); EXPECT_EQ(artists[1], artist4.getId());
} }
{ {
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "%MyArtist" }).setSortMethod(ArtistSortMethod::Name)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "%MyArtist" }).setSortMethod(ArtistSortMethod::Name)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId()); EXPECT_EQ(artists[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId()); EXPECT_EQ(artists[1], artist5.getId());
} }
{ {
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "_MyArtist" }).setSortMethod(ArtistSortMethod::Name)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "_MyArtist" }).setSortMethod(ArtistSortMethod::Name)) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId()); 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 allArtistsByName{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Name)) };
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName)) }; auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName)) };
ASSERT_EQ(allArtistsByName.results.size(), 2); ASSERT_EQ(allArtistsByName.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); EXPECT_EQ(allArtistsByName.front(), artistA.getId());
EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); EXPECT_EQ(allArtistsByName.back(), artistB.getId());
ASSERT_EQ(allArtistsBySortName.results.size(), 2); ASSERT_EQ(allArtistsBySortName.size(), 2);
EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); EXPECT_EQ(allArtistsBySortName.front(), artistB.getId());
EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); EXPECT_EQ(allArtistsBySortName.back(), artistA.getId());
} }
} }
@@ -955,7 +940,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setNonRelease(true).setArtist(artist->getId())) }; 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() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId()).setNonRelease(true)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId()).setNonRelease(true)) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track2.getId()); EXPECT_EQ(tracks.front(), track2.getId());
} }
} }
@@ -1033,11 +1018,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::AddedDesc)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::AddedDesc)) };
ASSERT_EQ(artists.results.size(), 4); ASSERT_EQ(artists.size(), 4);
EXPECT_EQ(artists.results[0], artistD.getId()); EXPECT_EQ(artists[0], artistD.getId());
EXPECT_EQ(artists.results[1], artistA.getId()); EXPECT_EQ(artists[1], artistA.getId());
EXPECT_EQ(artists.results[2], artistB.getId()); EXPECT_EQ(artists[2], artistB.getId());
EXPECT_EQ(artists.results[3], artistC.getId()); EXPECT_EQ(artists[3], artistC.getId());
} }
} }
@@ -1073,11 +1058,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::LastWrittenDesc)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::LastWrittenDesc)) };
ASSERT_EQ(artists.results.size(), 4); ASSERT_EQ(artists.size(), 4);
EXPECT_EQ(artists.results[0], artistA.getId()); EXPECT_EQ(artists[0], artistA.getId());
EXPECT_EQ(artists.results[1], artistD.getId()); EXPECT_EQ(artists[1], artistD.getId());
EXPECT_EQ(artists.results[2], artistB.getId()); EXPECT_EQ(artists[2], artistB.getId());
EXPECT_EQ(artists.results[3], artistC.getId()); EXPECT_EQ(artists[3], artistC.getId());
} }
} }
@@ -1144,8 +1129,8 @@ namespace lms::db::tests
ArtistId lastRetrievedArtist; ArtistId lastRetrievedArtist;
const auto results{ Artist::findWithMBIDNameVariants(session, lastRetrievedArtist) }; const auto results{ Artist::findWithMBIDNameVariants(session, lastRetrievedArtist) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), artistA.getId()); EXPECT_EQ(results[0]->getId(), artistA.getId());
EXPECT_EQ(lastRetrievedArtist, artistA.getId()); EXPECT_EQ(lastRetrievedArtist, artistA.getId());
} }
} }
+2
View File
@@ -16,6 +16,7 @@ add_executable(test-database
Medium.cpp Medium.cpp
Migration.cpp Migration.cpp
Mood.cpp Mood.cpp
Movement.cpp
PlayListFile.cpp PlayListFile.cpp
Podcast.cpp Podcast.cpp
RatedArtist.cpp RatedArtist.cpp
@@ -29,6 +30,7 @@ add_executable(test-database
StarredTrack.cpp StarredTrack.cpp
Track.cpp Track.cpp
TrackArtistLink.cpp TrackArtistLink.cpp
Work.cpp
TrackBookmark.cpp TrackBookmark.cpp
TrackEmbeddedImage.cpp TrackEmbeddedImage.cpp
TrackList.cpp TrackList.cpp
+90 -90
View File
@@ -50,26 +50,26 @@ namespace lms::db::tests
{ {
const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}) }; const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId()); EXPECT_EQ(clusters.front(), cluster.getId());
} }
{ {
const auto clusters{ Cluster::findOrphanIds(session) }; const auto clusters{ Cluster::findOrphanIds(session) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId()); EXPECT_EQ(clusters.front(), cluster.getId());
} }
auto clusterTypes{ ClusterType::findIds(session) }; auto clusterTypes{ ClusterType::findIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.front(), clusterType.getId());
clusterTypes = ClusterType::findUsed(session); clusterTypes = ClusterType::findUsed(session);
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.front(), clusterType.getId());
clusterTypes = ClusterType::findOrphanIds(session); 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 transaction{ session.createReadTransaction() };
auto clusterTypes{ ClusterType::findOrphanIds(session) }; auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); 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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0);
auto clusterTypes{ ClusterType::findOrphanIds(session) }; auto clusterTypes{ ClusterType::findOrphanIds(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.front(), clusterType.getId());
} }
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
@@ -167,7 +167,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto clusters{ Cluster::findOrphanIds(session) }; 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->getClusters().size(), 0);
EXPECT_EQ(track->getClusterIds().size(), 0); EXPECT_EQ(track->getClusterIds().size(), 0);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0);
@@ -183,8 +183,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setTrack(track.getId())) }; auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster1.getId()); EXPECT_EQ(clusters.front(), cluster1.getId());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
} }
@@ -192,21 +192,21 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto clusters{ Cluster::findOrphanIds(session) }; auto clusters{ Cluster::findOrphanIds(session) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster2.getId()); 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 transaction{ session.createReadTransaction() };
auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
tracks = Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }))); tracks = Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ 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() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; 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() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; 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, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
} }
@@ -264,8 +264,8 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1);
} }
@@ -289,11 +289,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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()); 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(); }) }; auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) };
EXPECT_TRUE(it != std::cend(tracks)); EXPECT_TRUE(it != std::cend(tracks));
@@ -333,8 +333,8 @@ namespace lms::db::tests
{ {
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}).results.size(), 0); EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}).size(), 0);
EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.size(), 0); EXPECT_EQ(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).size(), 0);
} }
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
@@ -342,18 +342,18 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}).results }; auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}) };
ASSERT_EQ(clusters.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.front(), cluster.getId()); 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); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.front(), cluster.getId()); 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); 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); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.front(), cluster.getId()); EXPECT_EQ(clusters.front(), cluster.getId());
} }
@@ -366,7 +366,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0);
} }
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
@@ -375,9 +375,9 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2); ASSERT_EQ(Cluster::findOrphanIds(session).size(), 2);
EXPECT_EQ(Release::find(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() }))).results.size(), 0); EXPECT_EQ(Release::find(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() }))).size(), 0);
EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); EXPECT_EQ(Release::find(session, Release::FindParameters{}).size(), 1);
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0);
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
} }
@@ -394,8 +394,8 @@ namespace lms::db::tests
{ {
auto clusters{ Cluster::findOrphanIds(session) }; auto clusters{ Cluster::findOrphanIds(session) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); EXPECT_EQ(clusters.front(), unusedCluster.getId());
} }
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
@@ -405,23 +405,23 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) }; const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId()); EXPECT_EQ(clusters.front(), cluster.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ unusedCluster.getId() }))) }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ 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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(ClusterType::findOrphanIds(session).results.size(), 0); EXPECT_EQ(ClusterType::findOrphanIds(session).size(), 0);
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 2);
EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Release::findOrphanIds(session).size(), 0);
EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Artist::findOrphanIds(session).size(), 0);
} }
{ {
@@ -467,11 +467,11 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }))).results.size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }))).size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).results.size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).size(), 0);
cluster2.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get());
} }
@@ -480,18 +480,18 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() }))); artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster2.getId() })));
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() }))); artists = Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId() })));
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).results.size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster3.getId() }))).size(), 0);
} }
} }
@@ -512,17 +512,17 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0);
EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Release::findOrphanIds(session).size(), 0);
EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Artist::findOrphanIds(session).size(), 0);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
} }
@@ -552,8 +552,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0);
EXPECT_EQ(Artist::findOrphanIds(session).results.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(); }); 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))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(clusterIds))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
} }
} }
@@ -587,26 +587,26 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Cluster::findOrphanIds(session).size(), 0);
EXPECT_EQ(ClusterType::findOrphanIds(session).results.size(), 0); EXPECT_EQ(ClusterType::findOrphanIds(session).size(), 0);
EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Artist::findOrphanIds(session).size(), 0);
EXPECT_EQ(Release::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Release::findOrphanIds(session).size(), 0);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) }; auto artists{ Artist::findIds(session, Artist::FindParameters{}.setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
} }
} }
@@ -632,12 +632,12 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() }))); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() })));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
} }
} }
@@ -743,17 +743,17 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto genres{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GENRE")) }; 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")) }; const auto moods{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("MOOD")) };
ASSERT_EQ(moods.results.size(), 1); ASSERT_EQ(moods.size(), 1);
EXPECT_EQ(moods.results.front()->getId(), mood.getId()); EXPECT_EQ(moods.front()->getId(), mood.getId());
const auto all{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.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")) }; const auto none{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GROUPING")) };
EXPECT_EQ(none.results.size(), 0); EXPECT_EQ(none.size(), 0);
} }
} }
-71
View File
@@ -74,26 +74,6 @@ namespace lms::db::tests
session.fullAnalyze(); session.fullAnalyze();
} }
TEST_F(DatabaseFixture, Common_subRangeEmpty)
{
RangeResults<int> 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) TEST_F(DatabaseFixture, Common_subRangeForeach)
{ {
struct TestCase struct TestCase
@@ -155,55 +135,4 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Common_subRange)
{
RangeResults<int> 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 } // namespace lms::db::tests
+9 -9
View File
@@ -42,19 +42,19 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Release::findOrphanIds(session).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).empty());
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).empty());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
const auto releaseTracks{ Track::find(session, Track::FindParameters{}.setRelease(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 transaction{ session.createWriteTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.front(), release.getId());
auto artists{ release->getTrackArtists() }; auto artists{ release->getTrackArtists() };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
@@ -88,7 +88,7 @@ namespace lms::db::tests
{ {
{ {
auto transaction{ session.createReadTransaction() }; 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); EXPECT_EQ(User::getCount(session), 0);
} }
@@ -97,7 +97,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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); EXPECT_EQ(User::getCount(session), 1);
} }
} }
+8 -8
View File
@@ -148,7 +148,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findOrphanIds(session).results }; const auto directories{ Directory::findOrphanIds(session) };
EXPECT_EQ(directories.size(), 2); EXPECT_EQ(directories.size(), 2);
} }
@@ -161,7 +161,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findOrphanIds(session).results }; const auto directories{ Directory::findOrphanIds(session) };
ASSERT_EQ(directories.size(), 1); ASSERT_EQ(directories.size(), 1);
EXPECT_EQ(directories.front(), child.getId()); EXPECT_EQ(directories.front(), child.getId());
} }
@@ -182,7 +182,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto directories{ Directory::findRootDirectories(session).results }; const auto directories{ Directory::findRootDirectories(session) };
ASSERT_EQ(directories.size(), 2); ASSERT_EQ(directories.size(), 2);
EXPECT_EQ(directories[0]->getId(), parent1.getId()); EXPECT_EQ(directories[0]->getId(), parent1.getId());
EXPECT_EQ(directories[1]->getId(), parent2.getId()); EXPECT_EQ(directories[1]->getId(), parent2.getId());
@@ -209,7 +209,7 @@ namespace lms::db::tests
Directory::FindParameters params; Directory::FindParameters params;
params.setWithNoTrack(true); params.setWithNoTrack(true);
auto res{ Directory::find(session, params).results }; auto res{ Directory::find(session, params) };
ASSERT_EQ(res.size(), 3); ASSERT_EQ(res.size(), 3);
EXPECT_EQ(res[0]->getId(), parent.getId()); EXPECT_EQ(res[0]->getId(), parent.getId());
@@ -228,7 +228,7 @@ namespace lms::db::tests
Directory::FindParameters params; Directory::FindParameters params;
params.setWithNoTrack(true); params.setWithNoTrack(true);
auto res{ Directory::find(session, params).results }; auto res{ Directory::find(session, params) };
ASSERT_EQ(res.size(), 2); ASSERT_EQ(res.size(), 2);
EXPECT_EQ(res[0]->getId(), parent.getId()); EXPECT_EQ(res[0]->getId(), parent.getId());
EXPECT_EQ(res[1]->getId(), child1.getId()); EXPECT_EQ(res[1]->getId(), child1.getId());
@@ -247,7 +247,7 @@ namespace lms::db::tests
Directory::FindParameters params; Directory::FindParameters params;
params.setKeywords({ "foo" }); params.setKeywords({ "foo" });
auto res{ Directory::find(session, params).results }; auto res{ Directory::find(session, params) };
ASSERT_EQ(res.size(), 2); ASSERT_EQ(res.size(), 2);
EXPECT_EQ(res[0]->getId(), child1.getId()); EXPECT_EQ(res[0]->getId(), child1.getId());
@@ -267,7 +267,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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); ASSERT_EQ(res.size(), 2);
EXPECT_EQ(res[0], parent1.getId()); EXPECT_EQ(res[0], parent1.getId());
EXPECT_EQ(res[1], child1.getId()); EXPECT_EQ(res[1], child1.getId());
@@ -283,7 +283,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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); EXPECT_EQ(res.size(), 0);
} }
} }
+22 -22
View File
@@ -65,8 +65,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto orphans{ Genre::findOrphanIds(session) }; const auto orphans{ Genre::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), genre.getId()); EXPECT_EQ(orphans.front(), genre.getId());
} }
} }
@@ -78,7 +78,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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->getGenres().size(), 0);
EXPECT_EQ(track->getGenreIds().size(), 0); EXPECT_EQ(track->getGenreIds().size(), 0);
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 0); EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 0);
@@ -94,15 +94,15 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setTrack(track.getId())) }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(genres.results.size(), 1); ASSERT_EQ(genres.size(), 1);
EXPECT_EQ(genres.results.front(), genre1.getId()); EXPECT_EQ(genres.front(), genre1.getId());
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1); EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0); EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0);
const auto orphans{ Genre::findOrphanIds(session) }; const auto orphans{ Genre::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), genre2.getId()); EXPECT_EQ(orphans.front(), genre2.getId());
const auto trackGenres{ track->getGenres() }; const auto trackGenres{ track->getGenres() };
ASSERT_EQ(trackGenres.size(), 1); ASSERT_EQ(trackGenres.size(), 1);
@@ -116,11 +116,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1); EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
EXPECT_EQ(Genre::computeTrackCount(session, genre2.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() }; const auto trackGenres{ track->getGenres() };
EXPECT_EQ(trackGenres.size(), 2); EXPECT_EQ(trackGenres.size(), 2);
@@ -148,12 +148,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) }; const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1); ASSERT_EQ(tracks2.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId()); EXPECT_EQ(tracks2.front(), track.getId());
} }
} }
@@ -200,10 +200,10 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::Name)) }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::Name)) };
ASSERT_EQ(genres.results.size(), 3); ASSERT_EQ(genres.size(), 3);
EXPECT_EQ(genres.results[0], g2.getId()); EXPECT_EQ(genres[0], g2.getId());
EXPECT_EQ(genres.results[1], g3.getId()); EXPECT_EQ(genres[1], g3.getId());
EXPECT_EQ(genres.results[2], g1.getId()); EXPECT_EQ(genres[2], g1.getId());
} }
} }
@@ -225,9 +225,9 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::TrackCountDesc)) }; const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::TrackCountDesc)) };
ASSERT_EQ(genres.results.size(), 2); ASSERT_EQ(genres.size(), 2);
EXPECT_EQ(genres.results[0], g1.getId()); EXPECT_EQ(genres[0], g1.getId());
EXPECT_EQ(genres.results[1], g2.getId()); EXPECT_EQ(genres[1], g2.getId());
} }
} }
} // namespace lms::db::tests } // namespace lms::db::tests
+22 -22
View File
@@ -63,8 +63,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto orphans{ Grouping::findOrphanIds(session) }; const auto orphans{ Grouping::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), grouping.getId()); EXPECT_EQ(orphans.front(), grouping.getId());
} }
} }
@@ -76,7 +76,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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->getGroupings().size(), 0);
EXPECT_EQ(track->getGroupingIds().size(), 0); EXPECT_EQ(track->getGroupingIds().size(), 0);
} }
@@ -90,12 +90,12 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setTrack(track.getId())) }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(groupings.results.size(), 1); ASSERT_EQ(groupings.size(), 1);
EXPECT_EQ(groupings.results.front(), grouping1.getId()); EXPECT_EQ(groupings.front(), grouping1.getId());
const auto orphans{ Grouping::findOrphanIds(session) }; const auto orphans{ Grouping::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), grouping2.getId()); EXPECT_EQ(orphans.front(), grouping2.getId());
const auto trackGroupings{ track->getGroupings() }; const auto trackGroupings{ track->getGroupings() };
ASSERT_EQ(trackGroupings.size(), 1); ASSERT_EQ(trackGroupings.size(), 1);
@@ -109,11 +109,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Grouping::findOrphanIds(session).size(), 0);
const auto trackGroupings{ track->getGroupings() }; const auto trackGroupings{ track->getGroupings() };
EXPECT_EQ(trackGroupings.size(), 2); EXPECT_EQ(trackGroupings.size(), 2);
@@ -139,12 +139,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) }; const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1); ASSERT_EQ(tracks2.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId()); EXPECT_EQ(tracks2.front(), track.getId());
} }
} }
@@ -157,10 +157,10 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::Name)) }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::Name)) };
ASSERT_EQ(groupings.results.size(), 3); ASSERT_EQ(groupings.size(), 3);
EXPECT_EQ(groupings.results[0], g2.getId()); EXPECT_EQ(groupings[0], g2.getId());
EXPECT_EQ(groupings.results[1], g3.getId()); EXPECT_EQ(groupings[1], g3.getId());
EXPECT_EQ(groupings.results[2], g1.getId()); EXPECT_EQ(groupings[2], g1.getId());
} }
} }
@@ -182,9 +182,9 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::TrackCountDesc)) }; const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::TrackCountDesc)) };
ASSERT_EQ(groupings.results.size(), 2); ASSERT_EQ(groupings.size(), 2);
EXPECT_EQ(groupings.results[0], g1.getId()); EXPECT_EQ(groupings[0], g1.getId());
EXPECT_EQ(groupings.results[1], g2.getId()); EXPECT_EQ(groupings[1], g2.getId());
} }
} }
} // namespace lms::db::tests } // namespace lms::db::tests
+18 -18
View File
@@ -83,7 +83,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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() }; 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); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.front()->getId(), image.getId()); EXPECT_EQ(results.front()->getId(), image.getId());
} }
@@ -138,8 +138,8 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem(fileStem); params.setFileStem(fileStem);
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -147,8 +147,8 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem(fileStem, Image::FindParameters::ProcessWildcards{ true }); params.setFileStem(fileStem, Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -156,7 +156,7 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem("nonexistent"); params.setFileStem("nonexistent");
const auto results{ Image::find(session, params) }; 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; Image::FindParameters params;
params.setFileStem("ima*"); params.setFileStem("ima*");
const auto results{ Image::find(session, params) }; 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; Image::FindParameters params;
params.setFileStem("ima*", Image::FindParameters::ProcessWildcards{ true }); params.setFileStem("ima*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -181,8 +181,8 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem("*ge", Image::FindParameters::ProcessWildcards{ true }); params.setFileStem("*ge", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -190,8 +190,8 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem("*g*", Image::FindParameters::ProcessWildcards{ true }); params.setFileStem("*g*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -199,8 +199,8 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem("*", Image::FindParameters::ProcessWildcards{ true }); params.setFileStem("*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1); ASSERT_EQ(results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId()); EXPECT_EQ(results[0]->getId(), image.getId());
} }
{ {
@@ -208,7 +208,7 @@ namespace lms::db::tests
Image::FindParameters params; Image::FindParameters params;
params.setFileStem("ima%"); params.setFileStem("ima%");
const auto results{ Image::find(session, params) }; 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; Image::FindParameters params;
params.setFileStem("ima%", Image::FindParameters::ProcessWildcards{ true }); params.setFileStem("ima%", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) }; const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 0); ASSERT_EQ(results.size(), 0);
} }
} }
+22 -22
View File
@@ -63,8 +63,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto orphans{ Language::findOrphanIds(session) }; const auto orphans{ Language::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), language.getId()); EXPECT_EQ(orphans.front(), language.getId());
} }
} }
@@ -76,7 +76,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; 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->getLanguages().size(), 0);
EXPECT_EQ(track->getLanguageIds().size(), 0); EXPECT_EQ(track->getLanguageIds().size(), 0);
} }
@@ -90,12 +90,12 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setTrack(track.getId())) }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(languages.results.size(), 1); ASSERT_EQ(languages.size(), 1);
EXPECT_EQ(languages.results.front(), language1.getId()); EXPECT_EQ(languages.front(), language1.getId());
const auto orphans{ Language::findOrphanIds(session) }; const auto orphans{ Language::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1); ASSERT_EQ(orphans.size(), 1);
EXPECT_EQ(orphans.results.front(), language2.getId()); EXPECT_EQ(orphans.front(), language2.getId());
const auto trackLanguages{ track->getLanguages() }; const auto trackLanguages{ track->getLanguages() };
ASSERT_EQ(trackLanguages.size(), 1); ASSERT_EQ(trackLanguages.size(), 1);
@@ -109,11 +109,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.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() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Language::findOrphanIds(session).results.size(), 0); EXPECT_EQ(Language::findOrphanIds(session).size(), 0);
const auto trackLanguages{ track->getLanguages() }; const auto trackLanguages{ track->getLanguages() };
EXPECT_EQ(trackLanguages.size(), 2); EXPECT_EQ(trackLanguages.size(), 2);
@@ -139,12 +139,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) }; const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1); ASSERT_EQ(tracks2.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId()); EXPECT_EQ(tracks2.front(), track.getId());
} }
} }
@@ -157,10 +157,10 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::Name)) }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::Name)) };
ASSERT_EQ(languages.results.size(), 3); ASSERT_EQ(languages.size(), 3);
EXPECT_EQ(languages.results[0], l2.getId()); EXPECT_EQ(languages[0], l2.getId());
EXPECT_EQ(languages.results[1], l3.getId()); EXPECT_EQ(languages[1], l3.getId());
EXPECT_EQ(languages.results[2], l1.getId()); EXPECT_EQ(languages[2], l1.getId());
} }
} }
@@ -182,9 +182,9 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::TrackCountDesc)) }; const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::TrackCountDesc)) };
ASSERT_EQ(languages.results.size(), 2); ASSERT_EQ(languages.size(), 2);
EXPECT_EQ(languages.results[0], l1.getId()); EXPECT_EQ(languages[0], l1.getId());
EXPECT_EQ(languages.results[1], l2.getId()); EXPECT_EQ(languages[1], l2.getId());
} }
} }
} // namespace lms::db::tests } // namespace lms::db::tests

Some files were not shown because too many files have changed in this diff Show More