Merge branch 'develop' for release v3.78.0

This commit is contained in:
emeric
2026-06-23 22:30:41 +02:00
188 changed files with 7848 additions and 1465 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.76.0) project(lms VERSION 3.78.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
@@ -1,8 +1,9 @@
FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx
FROM --platform=$BUILDPLATFORM alpine:3.23 FROM --platform=$BUILDPLATFORM alpine:3.24
ARG BUILD_PACKAGES="\ ARG BUILD_PACKAGES="\
clang \ clang \
llvm \
make \ make \
cmake \ cmake \
lld \ lld \
+3 -3
View File
@@ -1,4 +1,4 @@
FROM alpine:3.23 AS build FROM alpine:3.24 AS build
WORKDIR /tmp/workdir WORKDIR /tmp/workdir
@@ -81,7 +81,7 @@ RUN \
make -j$(nproc) install make -j$(nproc) install
# WT # WT
ARG WT_VERSION=4.13.2 ARG WT_VERSION=4.13.3
ARG WT_DEBUG=OFF ARG WT_DEBUG=OFF
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
@@ -147,7 +147,7 @@ RUN \
rm -rf /tmp/fakeroot/share/Wt/resources/themes rm -rf /tmp/fakeroot/share/Wt/resources/themes
## Release Stage ## Release Stage
FROM alpine:3.23 AS release FROM alpine:3.24 AS release
LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>" LABEL maintainer="Emeric Poupon <itmfr@yahoo.fr>"
ARG RUNTIME_PACKAGES=" \ ARG RUNTIME_PACKAGES=" \
+1 -1
View File
@@ -41,7 +41,7 @@ _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:
* **Tag-based**: uses metadata tags `genre` and `grouping`. * **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). * **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
+1
View File
@@ -75,6 +75,7 @@ The following extra fields are implemented:
* [getPodcastEpisode](https://opensubsonic.netlify.app/docs/extensions/getpodcastepisode/) * [getPodcastEpisode](https://opensubsonic.netlify.app/docs/extensions/getpodcastepisode/)
* [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/) * [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/)
* [Index based Queue](https://opensubsonic.netlify.app/docs/extensions/indexbasedqueue/) * [Index based Queue](https://opensubsonic.netlify.app/docs/extensions/indexbasedqueue/)
* [Sonic similarity](https://opensubsonic.netlify.app/docs/extensions/sonicsimilarity/)
* [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/) * [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/)
* [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/) * [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/)
* [Transcoding](https://opensubsonic.netlify.app/docs/extensions/transcoding/) * [Transcoding](https://opensubsonic.netlify.app/docs/extensions/transcoding/)
+15
View File
@@ -8,8 +8,23 @@
<hr/> <hr/>
<legend>${tr:Lms.Admin.DebugTools.Db.db}</legend> <legend>${tr:Lms.Admin.DebugTools.Db.db}</legend>
${db} ${db}
<hr/>
<legend>${tr:Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats}</legend>
${pcm-decoding-stats}
</form> </form>
</message> </message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.template">
<form>
<div class="row g-3">
<div class="col-12">
${run-btn class="btn btn-primary"}
${download-btn class="btn btn-secondary ms-2"}
${status}
</div>
</div>
</form>
</message>
</messages> </messages>
-2
View File
@@ -45,10 +45,8 @@
<li>${scanner class="dropdown-item"}</li> <li>${scanner class="dropdown-item"}</li>
<li>${users class="dropdown-item"}</li> <li>${users class="dropdown-item"}</li>
<li>${about class="dropdown-item"}</li> <li>${about class="dropdown-item"}</li>
${<if-debug-tools>}
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li>${debug-tools class="dropdown-item"}</li> <li>${debug-tools class="dropdown-item"}</li>
${</if-debug-tools>}
</ul> </ul>
</li> </li>
${</if-is-admin>} ${</if-is-admin>}
+42 -3
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Get report</message> <message id="Lms.Admin.ScannerController.get-report">Get report</message>
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message> <message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates</message> <message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} - {5} errors, {6} duplicates</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Missing artist name</message> <message id="Lms.Admin.ScannerController.missing-artist-name">Missing artist name</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Unable to find track paths</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Unable to find track paths</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Unable to find the path "{1}"</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">Unable to find the path "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Status</message> <message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled at {2} on {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">Scheduled at {2} on {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracting MusicNN embeddings: {1} of {2} files ({3}%)</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracting MusicNN embeddings: {1} of {2} files ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Export query profiling data</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">Export query profiling data</message>
<message id="Lms.Admin.DebugTools.Db.db">Database</message> <message id="Lms.Admin.DebugTools.Db.db">Database</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">PCM decoding stats</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Run bench</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Download results</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Running…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">PCM decoding benchmark completed in {1}</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Export traces</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Export traces</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message>
@@ -221,7 +228,19 @@
<message id="Lms.Explore.duration">Duration</message> <message id="Lms.Explore.duration">Duration</message>
<message id="Lms.Explore.filter-added">Filter added</message> <message id="Lms.Explore.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</message> <message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.genre">
<plural case="0">Genre</plural>
<plural case="1">Genres</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Grouping</plural>
<plural case="1">Groupings</plural>
</message>
<message id="Lms.Explore.label">Label</message> <message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.language">
<plural case="0">Language</plural>
<plural case="1">Languages</plural>
</message>
<message id="Lms.Explore.lyricist"> <message id="Lms.Explore.lyricist">
<plural case="0">Lyricist</plural> <plural case="0">Lyricist</plural>
<plural case="1">Lyricists</plural> <plural case="1">Lyricists</plural>
@@ -231,6 +250,10 @@
<plural case="0">Mixer</plural> <plural case="0">Mixer</plural>
<plural case="1">Mixers</plural> <plural case="1">Mixers</plural>
</message> </message>
<message id="Lms.Explore.mood">
<plural case="0">Mood</plural>
<plural case="1">Moods</plural>
</message>
<message id="Lms.Explore.most-played">Most played</message> <message id="Lms.Explore.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
@@ -343,8 +366,24 @@
<message id="Lms.Settings.audio">Audio</message> <message id="Lms.Settings.audio">Audio</message>
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser</message> <message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser</message>
<message id="Lms.Settings.backend.internal">Internal</message> <message id="Lms.Settings.backend.internal">Internal</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Last.fm API key</message>
<message id="Lms.Settings.services.lastfm-api-secret">Last.fm API secret</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
<message id="Lms.Settings.services.lastfm-card-title">Last.fm account</message>
<message id="Lms.Settings.services.lastfm-linked">Linked</message>
<message id="Lms.Settings.services.lastfm-link">Link account</message>
<message id="Lms.Settings.services.lastfm-unlink">Unlink account</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">Link Last.fm account</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">Enter your Last.fm API key and shared secret (get them at <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a>), then click Authorize.</message>
<message id="Lms.Settings.services.lastfm-authorize">Authorize on Last.fm</message>
<message id="Lms.Settings.services.lastfm-done">Done, I've authorized</message>
<message id="Lms.Settings.services.lastfm-auth-url">Didn't open? Click here to authorize on Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">Unlink Last.fm account</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">Are you sure you want to unlink your Last.fm account? Scrobbling will stop until you link again.</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">Please enter your Last.fm API key and secret.</message>
<message id="Lms.Settings.services.lastfm-auth-error">Last.fm authentication failed. Check your API key and secret.</message>
<message id="Lms.Settings.change-password">Change password</message> <message id="Lms.Settings.change-password">Change password</message>
<message id="Lms.Settings.date-asc">Release Date (Ascending)</message> <message id="Lms.Settings.date-asc">Release Date (Ascending)</message>
<message id="Lms.Settings.date-desc">Release Date (Descending)</message> <message id="Lms.Settings.date-desc">Release Date (Descending)</message>
+42 -3
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Informe</message> <message id="Lms.Admin.ScannerController.get-report">Informe</message>
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message> <message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message> <message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} - {5} errores, {6} duplicados</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Falta el nombre del artista</message> <message id="Lms.Admin.ScannerController.missing-artist-name">Falta el nombre del artista</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">No se pudieron encontrar las rutas de las pistas</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">No se pudieron encontrar las rutas de las pistas</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">No se pudo encontrar la ruta "{1}"</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">No se pudo encontrar la ruta "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Escanear</message> <message id="Lms.Admin.ScannerController.scanner">Escanear</message>
<message id="Lms.Admin.ScannerController.status">Estado</message> <message id="Lms.Admin.ScannerController.status">Estado</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">No progamado</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">No progamado</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programado el {1} a las {2} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">Programado el {1} a las {2}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Escaneando: etapa {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Escaneando: etapa {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message> <message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calculando estadísticas... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracción de embeddings de MusicNN: {1} de {2} archivos ({3}%)</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extracción de embeddings de MusicNN: {1} de {2} archivos ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exportar datos de perfil de consultas</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exportar datos de perfil de consultas</message>
<message id="Lms.Admin.DebugTools.Db.db">Base de datos</message> <message id="Lms.Admin.DebugTools.Db.db">Base de datos</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Estadísticas de decodificación PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Ejecutar benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Descargar resultados</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Ejecutando…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark de decodificación PCM completado en {1}</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exportar las trazas</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exportar las trazas</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Trazas</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">Trazas</message>
@@ -221,7 +228,19 @@
<message id="Lms.Explore.duration">Duración</message> <message id="Lms.Explore.duration">Duración</message>
<message id="Lms.Explore.filter-added">Filtro añadido</message> <message id="Lms.Explore.filter-added">Filtro añadido</message>
<message id="Lms.Explore.filters">Filtros</message> <message id="Lms.Explore.filters">Filtros</message>
<message id="Lms.Explore.genre">
<plural case="0">Género</plural>
<plural case="1">Géneros</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Agrupación</plural>
<plural case="1">Agrupaciones</plural>
</message>
<message id="Lms.Explore.label">Sello discográfico</message> <message id="Lms.Explore.label">Sello discográfico</message>
<message id="Lms.Explore.language">
<plural case="0">Idioma</plural>
<plural case="1">Idiomas</plural>
</message>
<message id="Lms.Explore.lyricist"> <message id="Lms.Explore.lyricist">
<plural case="0">Letrista</plural> <plural case="0">Letrista</plural>
<plural case="1">Letristas</plural> <plural case="1">Letristas</plural>
@@ -231,6 +250,10 @@
<plural case="0">Mezclador</plural> <plural case="0">Mezclador</plural>
<plural case="1">Mezcladores</plural> <plural case="1">Mezcladores</plural>
</message> </message>
<message id="Lms.Explore.mood">
<plural case="0">Estado de ánimo</plural>
<plural case="1">Estados de ánimo</plural>
</message>
<message id="Lms.Explore.most-played">Reproducidos frecuentemente</message> <message id="Lms.Explore.most-played">Reproducidos frecuentemente</message>
<message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</message> <message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Álbum de MusicBrainz</message> <message id="Lms.Explore.musicbrainz-release">Álbum de MusicBrainz</message>
@@ -345,6 +368,22 @@
<message id="Lms.Settings.backend.internal">Interno</message> <message id="Lms.Settings.backend.internal">Interno</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Token de la API de ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Token de la API de ListenBrainz</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Clave API de Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-secret">Secreto de API de Last.fm</message>
<message id="Lms.Settings.services.lastfm-card-title">Cuenta de Last.fm</message>
<message id="Lms.Settings.services.lastfm-linked">Vinculado</message>
<message id="Lms.Settings.services.lastfm-link">Vincular cuenta</message>
<message id="Lms.Settings.services.lastfm-unlink">Desvincular cuenta</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">Vincular cuenta de Last.fm</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">Introduce tu clave API y secreto compartido de Last.fm (obténlos en <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a>), luego haz clic en Autorizar.</message>
<message id="Lms.Settings.services.lastfm-authorize">Autorizar en Last.fm</message>
<message id="Lms.Settings.services.lastfm-done">Hecho, he autorizado</message>
<message id="Lms.Settings.services.lastfm-auth-url">¿No se abrió? Haz clic aquí para autorizar en Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">Desvincular cuenta de Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">¿Estás seguro de que deseas desvincular tu cuenta de Last.fm? El scrobbling se detendrá hasta que vuelvas a vincularla.</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">Por favor, introduce tu clave API y secreto de Last.fm.</message>
<message id="Lms.Settings.services.lastfm-auth-error">Error de autenticación de Last.fm. Comprueba tu clave API y secreto.</message>
<message id="Lms.Settings.change-password">Cambiar contraseña</message> <message id="Lms.Settings.change-password">Cambiar contraseña</message>
<message id="Lms.Settings.date-asc">Fecha de publicación (Ascendiente)</message> <message id="Lms.Settings.date-asc">Fecha de publicación (Ascendiente)</message>
<message id="Lms.Settings.date-desc">Fecha de publicación (Descendiente)</message> <message id="Lms.Settings.date-desc">Fecha de publicación (Descendiente)</message>
+48 -9
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Rapport</message> <message id="Lms.Admin.ScannerController.get-report">Rapport</message>
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message> <message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas</message> <message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} - {5} erreurs, {6} duplicatas</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Nom de l'artiste manquant</message> <message id="Lms.Admin.ScannerController.missing-artist-name">Nom de l'artiste manquant</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossible de trouver les chemins des pistes</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossible de trouver les chemins des pistes</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossible de trouver le chemin "{1}"</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">Impossible de trouver le chemin "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Statut</message> <message id="Lms.Admin.ScannerController.status">Statut</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié à {2} le {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">Planifié à {2} le {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message> <message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extraction des embeddings MusicNN : {1} sur {2} fichiers ({3}%)</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Extraction des embeddings MusicNN : {1} sur {2} fichiers ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exporter les données de profilage des requêtes</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">Exporter les données de profilage des requêtes</message>
<message id="Lms.Admin.DebugTools.Db.db">Base de données</message> <message id="Lms.Admin.DebugTools.Db.db">Base de données</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statistiques de décodage PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Lancer le bench</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Télécharger les résultats</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">En cours…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark de décodage PCM terminé en {1}</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exporter les traces</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Exporter les traces</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Traces</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">Traces</message>
@@ -221,7 +228,19 @@
<message id="Lms.Explore.duration">Durée</message> <message id="Lms.Explore.duration">Durée</message>
<message id="Lms.Explore.filter-added">Filtre ajouté</message> <message id="Lms.Explore.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</message> <message id="Lms.Explore.filters">Filtres</message>
<message id="Lms.Explore.genre">
<plural case="0">Genre</plural>
<plural case="1">Genres</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Regroupement</plural>
<plural case="1">Regroupements</plural>
</message>
<message id="Lms.Explore.label">Label</message> <message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.language">
<plural case="0">Langue</plural>
<plural case="1">Langues</plural>
</message>
<message id="Lms.Explore.lyricist"> <message id="Lms.Explore.lyricist">
<plural case="0">Parolier</plural> <plural case="0">Parolier</plural>
<plural case="1">Paroliers</plural> <plural case="1">Paroliers</plural>
@@ -231,6 +250,10 @@
<plural case="0">Mixeur</plural> <plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural> <plural case="1">Mixeurs</plural>
</message> </message>
<message id="Lms.Explore.mood">
<plural case="0">Ambiance</plural>
<plural case="1">Ambiances</plural>
</message>
<message id="Lms.Explore.most-played">Plus joués</message> <message id="Lms.Explore.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
@@ -345,6 +368,22 @@
<message id="Lms.Settings.backend.internal">Interne</message> <message id="Lms.Settings.backend.internal">Interne</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Clef API Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-secret">Secret API Last.fm</message>
<message id="Lms.Settings.services.lastfm-card-title">Compte Last.fm</message>
<message id="Lms.Settings.services.lastfm-linked">Lié</message>
<message id="Lms.Settings.services.lastfm-link">Lier le compte</message>
<message id="Lms.Settings.services.lastfm-unlink">Délier le compte</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">Lier le compte Last.fm</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">Entrez votre clef API et votre secret partagé Last.fm (obtenez-les sur <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a>), puis cliquez sur Autoriser.</message>
<message id="Lms.Settings.services.lastfm-authorize">Autoriser sur Last.fm</message>
<message id="Lms.Settings.services.lastfm-done">Terminé, j'ai autorisé</message>
<message id="Lms.Settings.services.lastfm-auth-url">Pas ouvert ? Cliquez ici pour autoriser sur Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">Délier le compte Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">Êtes-vous sûr de vouloir délier votre compte Last.fm ? Le scrobbling s'arrêtera jusqu'à ce que vous le reliiez.</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">Veuillez entrer votre clef API et votre secret Last.fm.</message>
<message id="Lms.Settings.services.lastfm-auth-error">Échec de l'authentification Last.fm. Vérifiez votre clef API et votre secret.</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message> <message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.date-asc">Date de sortie (Croissant)</message> <message id="Lms.Settings.date-asc">Date de sortie (Croissant)</message>
<message id="Lms.Settings.date-desc">Date de sortie (Décroissant)</message> <message id="Lms.Settings.date-desc">Date de sortie (Décroissant)</message>
@@ -366,13 +405,13 @@
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message> <message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message> <message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
<message id="Lms.Settings.generate-token">Regénérer</message> <message id="Lms.Settings.generate-token">Regénérer</message>
<message id="Lms.Settings.subsonic-token-copied">Clé API copiée dans le presse-papiers !</message> <message id="Lms.Settings.subsonic-token-copied">Clef API copiée dans le presse-papiers !</message>
<message id="Lms.Settings.subsonic-token-del-confirm">Supprimer</message> <message id="Lms.Settings.subsonic-token-del-confirm">Supprimer</message>
<message id="Lms.Settings.subsonic-token-del-confirm-body">Supprimer la clé déconnectera tous les clients qui l'utilisent actuellement.</message> <message id="Lms.Settings.subsonic-token-del-confirm-body">Supprimer la clef déconnectera tous les clients qui l'utilisent actuellement.</message>
<message id="Lms.Settings.subsonic-token-del-confirm-title">Supprimer la clé API ?</message> <message id="Lms.Settings.subsonic-token-del-confirm-title">Supprimer la clef API ?</message>
<message id="Lms.Settings.subsonic-token-regen-confirm">Regénérer</message> <message id="Lms.Settings.subsonic-token-regen-confirm">Regénérer</message>
<message id="Lms.Settings.subsonic-token-regen-confirm-body">Générer une nouvelle clé invalidera tous les clients utilisant actuellement la clé existante.</message> <message id="Lms.Settings.subsonic-token-regen-confirm-body">Générer une nouvelle clef invalidera tous les clients utilisant actuellement la clef existante.</message>
<message id="Lms.Settings.subsonic-token-regen-confirm-title">Regénérer la clé API ?</message> <message id="Lms.Settings.subsonic-token-regen-confirm-title">Regénérer la clef API ?</message>
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message> <message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">Pas de ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">Automatique</message> <message id="Lms.Settings.replaygain-mode.auto">Automatique</message>
@@ -390,7 +429,7 @@
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
<message id="Lms.Settings.subsonic-api">API Subsonic</message> <message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.subsonic-token">Clef pour l'API OpenSubsonic</message> <message id="Lms.Settings.subsonic-token">Clef pour l'API OpenSubsonic</message>
<message id="Lms.Settings.subsonic-token-usage">Utilisez cette clé d'API comme mot de passe pour les clients qui ne prennent pas en charge l'extension 'API Key Authentication'</message> <message id="Lms.Settings.subsonic-token-usage">Utilisez cette clef d'API comme mot de passe pour les clients qui ne prennent pas en charge l'extension 'API Key Authentication'</message>
<message id="Lms.Settings.transcoding">Transcodage</message> <message id="Lms.Settings.transcoding">Transcodage</message>
<message id="Lms.Settings.transcoding-mode">Appliquer le transcodage</message> <message id="Lms.Settings.transcoding-mode">Appliquer le transcodage</message>
<message id="Lms.Settings.transcoding-mode.always">Toujours</message> <message id="Lms.Settings.transcoding-mode.always">Toujours</message>
+42 -3
View File
@@ -129,7 +129,7 @@
<message id="Lms.Admin.ScannerController.get-report">Scarica un resoconto</message> <message id="Lms.Admin.ScannerController.get-report">Scarica un resoconto</message>
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message> <message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati</message> <message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} - {5} errori, {6} duplicati</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Nome dell'artista mancante</message> <message id="Lms.Admin.ScannerController.missing-artist-name">Nome dell'artista mancante</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossibile trovare i percorsi delle tracce</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossibile trovare i percorsi delle tracce</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossibile trovare il percorso "{1}"</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">Impossibile trovare il percorso "{1}"</message>
@@ -141,7 +141,7 @@
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Stato</message> <message id="Lms.Admin.ScannerController.status">Stato</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Programmato alle {2} del {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">Programmato alle {2} del {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
@@ -152,7 +152,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">Calcolo statistiche... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Estrazione degli embeddings MusicNN: {1} di {2} file ({3}%)</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Estrazione degli embeddings MusicNN: {1} di {2} file ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
@@ -169,6 +169,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Esporta i dati di profilazione delle query</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">Esporta i dati di profilazione delle query</message>
<message id="Lms.Admin.DebugTools.Db.db">Database</message> <message id="Lms.Admin.DebugTools.Db.db">Database</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statistiche di decodifica PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Avvia benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Scarica risultati</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">In esecuzione…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Benchmark di decodifica PCM completato in {1}</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Esporta tracce</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Esporta tracce</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">Tracing</message>
@@ -221,7 +228,19 @@
<message id="Lms.Explore.duration">Durata</message> <message id="Lms.Explore.duration">Durata</message>
<message id="Lms.Explore.filter-added">Filtro aggiunto</message> <message id="Lms.Explore.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</message> <message id="Lms.Explore.filters">Filtri</message>
<message id="Lms.Explore.genre">
<plural case="0">Genere</plural>
<plural case="1">Generi</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Raggruppamento</plural>
<plural case="1">Raggruppamenti</plural>
</message>
<message id="Lms.Explore.label">Etichetta</message> <message id="Lms.Explore.label">Etichetta</message>
<message id="Lms.Explore.language">
<plural case="0">Lingua</plural>
<plural case="1">Lingue</plural>
</message>
<message id="Lms.Explore.lyricist"> <message id="Lms.Explore.lyricist">
<plural case="0">Paroliere</plural> <plural case="0">Paroliere</plural>
<plural case="1">Parolieri</plural> <plural case="1">Parolieri</plural>
@@ -231,6 +250,10 @@
<plural case="0">Tecnico del mixaggio</plural> <plural case="0">Tecnico del mixaggio</plural>
<plural case="1">Tecnici del mixaggio</plural> <plural case="1">Tecnici del mixaggio</plural>
</message> </message>
<message id="Lms.Explore.mood">
<plural case="0">Umore</plural>
<plural case="1">Umori</plural>
</message>
<message id="Lms.Explore.most-played">Più ascoltati</message> <message id="Lms.Explore.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message> <message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message> <message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message>
@@ -345,6 +368,22 @@
<message id="Lms.Settings.backend.internal">Interno</message> <message id="Lms.Settings.backend.internal">Interno</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Chiave API Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-secret">Segreto API Last.fm</message>
<message id="Lms.Settings.services.lastfm-card-title">Account Last.fm</message>
<message id="Lms.Settings.services.lastfm-linked">Collegato</message>
<message id="Lms.Settings.services.lastfm-link">Collega account</message>
<message id="Lms.Settings.services.lastfm-unlink">Scollega account</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">Collega account Last.fm</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">Inserisci la chiave API e il segreto condiviso di Last.fm (ottenili su <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a>), poi clicca su Autorizza.</message>
<message id="Lms.Settings.services.lastfm-authorize">Autorizza su Last.fm</message>
<message id="Lms.Settings.services.lastfm-done">Fatto, ho autorizzato</message>
<message id="Lms.Settings.services.lastfm-auth-url">Non si è aperto? Clicca qui per autorizzare su Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">Scollega account Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">Sei sicuro di voler scollegare il tuo account Last.fm? Lo scrobbling si interromperà finché non lo ricolleghi.</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">Inserisci la chiave API e il segreto Last.fm.</message>
<message id="Lms.Settings.services.lastfm-auth-error">Autenticazione Last.fm fallita. Controlla la chiave API e il segreto.</message>
<message id="Lms.Settings.change-password">Cambia password</message> <message id="Lms.Settings.change-password">Cambia password</message>
<message id="Lms.Settings.date-asc">Data di uscita (Crescente)</message> <message id="Lms.Settings.date-asc">Data di uscita (Crescente)</message>
<message id="Lms.Settings.date-desc">Data di uscita (Decrescente)</message> <message id="Lms.Settings.date-desc">Data di uscita (Decrescente)</message>
+46 -3
View File
@@ -138,7 +138,7 @@
<message id="Lms.Admin.ScannerController.get-report">Pokaż raport</message> <message id="Lms.Admin.ScannerController.get-report">Pokaż raport</message>
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message> <message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów</message> <message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} - {5} błędów, {6} duplikatów</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">Brakuje nazwy artysty</message> <message id="Lms.Admin.ScannerController.missing-artist-name">Brakuje nazwy artysty</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Nie można znaleźć ścieżek utworów</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Nie można znaleźć ścieżek utworów</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">Nie można znaleźć ścieżki „{1}”</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">Nie można znaleźć ścieżki „{1}”</message>
@@ -150,7 +150,7 @@
<message id="Lms.Admin.ScannerController.scanner">Skaner</message> <message id="Lms.Admin.ScannerController.scanner">Skaner</message>
<message id="Lms.Admin.ScannerController.status">Status</message> <message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Nie zaplanowano</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Nie zaplanowano</message>
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano o {2} dnia {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano o {2} dnia {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
@@ -165,7 +165,7 @@
</message> </message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message> <message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">Obliczanie statystyk... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Ekstrakcja embeddingów MusicNN: {1} z {2} plików ({3}%)</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">Ekstrakcja embeddingów MusicNN: {1} z {2} plików ({3}%)</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
@@ -186,6 +186,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">Eksportuj dane profilowania zapytań</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">Eksportuj dane profilowania zapytań</message>
<message id="Lms.Admin.DebugTools.Db.db">Baza danych</message> <message id="Lms.Admin.DebugTools.Db.db">Baza danych</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">Statystyki dekodowania PCM</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">Uruchom benchmark</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">Pobierz wyniki</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">Trwa…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">Test wydajności dekodowania PCM zakończony w {1}</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Eksportuj ślady</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">Eksportuj ślady</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">Śledzenie</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">Śledzenie</message>
@@ -240,7 +247,22 @@
<message id="Lms.Explore.duration">Długość</message> <message id="Lms.Explore.duration">Długość</message>
<message id="Lms.Explore.filter-added">Filtr dodany</message> <message id="Lms.Explore.filter-added">Filtr dodany</message>
<message id="Lms.Explore.filters">Filtry</message> <message id="Lms.Explore.filters">Filtry</message>
<message id="Lms.Explore.genre">
<plural case="0">Gatunek</plural>
<plural case="1">Gatunki</plural>
<plural case="2">Gatunków</plural>
</message>
<message id="Lms.Explore.grouping">
<plural case="0">Grupowanie</plural>
<plural case="1">Grupowania</plural>
<plural case="2">Grupowań</plural>
</message>
<message id="Lms.Explore.label">Wytwórnia</message> <message id="Lms.Explore.label">Wytwórnia</message>
<message id="Lms.Explore.language">
<plural case="0">Język</plural>
<plural case="1">Języki</plural>
<plural case="2">Języków</plural>
</message>
<message id="Lms.Explore.lyricist"> <message id="Lms.Explore.lyricist">
<plural case="0">Tekściarz</plural> <plural case="0">Tekściarz</plural>
<plural case="1">Tekściarze</plural> <plural case="1">Tekściarze</plural>
@@ -252,6 +274,11 @@
<plural case="1">Mikserzy</plural> <plural case="1">Mikserzy</plural>
<plural case="2">Mikserów</plural> <plural case="2">Mikserów</plural>
</message> </message>
<message id="Lms.Explore.mood">
<plural case="0">Nastrój</plural>
<plural case="1">Nastroje</plural>
<plural case="2">Nastrojów</plural>
</message>
<message id="Lms.Explore.most-played">Najczęściej odtwarzane</message> <message id="Lms.Explore.most-played">Najczęściej odtwarzane</message>
<message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</message> <message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Wydanie w MusicBrainz</message> <message id="Lms.Explore.musicbrainz-release">Wydanie w MusicBrainz</message>
@@ -371,6 +398,22 @@
<message id="Lms.Settings.backend.internal">Wewnętrzny</message> <message id="Lms.Settings.backend.internal">Wewnętrzny</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Klucz API Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-secret">Sekret API Last.fm</message>
<message id="Lms.Settings.services.lastfm-card-title">Konto Last.fm</message>
<message id="Lms.Settings.services.lastfm-linked">Połączono</message>
<message id="Lms.Settings.services.lastfm-link">Połącz konto</message>
<message id="Lms.Settings.services.lastfm-unlink">Odłącz konto</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">Połącz konto Last.fm</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">Wprowadź klucz API i współdzielony sekret Last.fm (uzyskaj je na <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a>), następnie kliknij Autoryzuj.</message>
<message id="Lms.Settings.services.lastfm-authorize">Autoryzuj na Last.fm</message>
<message id="Lms.Settings.services.lastfm-done">Gotowe, autoryzowałem</message>
<message id="Lms.Settings.services.lastfm-auth-url">Nie otworzyło się? Kliknij tutaj, aby autoryzować na Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">Odłącz konto Last.fm</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">Czy na pewno chcesz odłączyć swoje konto Last.fm? Scrobbling zostanie zatrzymany do czasu ponownego połączenia.</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">Wprowadź klucz API i sekret Last.fm.</message>
<message id="Lms.Settings.services.lastfm-auth-error">Uwierzytelnianie Last.fm nie powiodło się. Sprawdź klucz API i sekret.</message>
<message id="Lms.Settings.change-password">Zmień hasło</message> <message id="Lms.Settings.change-password">Zmień hasło</message>
<message id="Lms.Settings.date-asc">Data wydania (Rosnąco)</message> <message id="Lms.Settings.date-asc">Data wydania (Rosnąco)</message>
<message id="Lms.Settings.date-desc">Data wydania (Malejąco)</message> <message id="Lms.Settings.date-desc">Data wydania (Malejąco)</message>
+30 -3
View File
@@ -127,7 +127,7 @@
<message id="Lms.Admin.ScannerController.get-report">生成报告</message> <message id="Lms.Admin.ScannerController.get-report">生成报告</message>
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message> <message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message> <message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} - {5} 个错误,{6} 个重复项</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">缺少艺术家名称</message> <message id="Lms.Admin.ScannerController.missing-artist-name">缺少艺术家名称</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">无法找到曲目路径</message> <message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">无法找到曲目路径</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">无法找到路径 "{1}"</message> <message id="Lms.Admin.ScannerController.playlist-path-missing">无法找到路径 "{1}"</message>
@@ -139,7 +139,7 @@
<message id="Lms.Admin.ScannerController.scanner">扫描器</message> <message id="Lms.Admin.ScannerController.scanner">扫描器</message>
<message id="Lms.Admin.ScannerController.status">状态</message> <message id="Lms.Admin.ScannerController.status">状态</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message>
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1}</message>
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">关联艺术家图片:{1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-artist-images">关联艺术家图片:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">关联外部歌词:{1}%...</message> <message id="Lms.Admin.ScannerController.step-associating-external-lyrics">关联外部歌词:{1}%...</message>
@@ -150,7 +150,7 @@
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">检查重复文件... {1} 个文件</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">检查重复文件... {1} 个文件</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message> <message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">正在计算统计信息... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-genre-stats">正在计算统计信息... {1}%</message>
<message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">正在提取 MusicNN 嵌入:{1} / {2} 个文件({3}%</message> <message id="Lms.Admin.ScannerController.step-extract-musicnn-embeddings">正在提取 MusicNN 嵌入:{1} / {2} 个文件({3}%</message>
<message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message>
@@ -167,6 +167,13 @@
<message id="Lms.Admin.DebugTools.Db.export-query-profiling">导出查询分析数据</message> <message id="Lms.Admin.DebugTools.Db.export-query-profiling">导出查询分析数据</message>
<message id="Lms.Admin.DebugTools.Db.db">数据库</message> <message id="Lms.Admin.DebugTools.Db.db">数据库</message>
<!--PcmDecodingStats-->
<message id="Lms.Admin.DebugTools.PcmDecodingStats.pcm-decoding-stats">PCM解码统计</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.run">运行基准测试</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.download">下载结果</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.running">运行中…</message>
<message id="Lms.Admin.DebugTools.PcmDecodingStats.benchmark-completed">PCM解码基准测试已在{1}内完成</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">导出跟踪</message> <message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">导出跟踪</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">跟踪</message> <message id="Lms.Admin.DebugTools.Tracing.tracing">跟踪</message>
@@ -213,10 +220,14 @@
<message id="Lms.Explore.duration">时长</message> <message id="Lms.Explore.duration">时长</message>
<message id="Lms.Explore.filter-added">过滤器已添加</message> <message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message> <message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.genre">流派</message>
<message id="Lms.Explore.grouping">分组</message>
<message id="Lms.Explore.label">厂牌</message> <message id="Lms.Explore.label">厂牌</message>
<message id="Lms.Explore.language">语言</message>
<message id="Lms.Explore.lyricist">作词者</message> <message id="Lms.Explore.lyricist">作词者</message>
<message id="Lms.Explore.media-library">音乐库</message> <message id="Lms.Explore.media-library">音乐库</message>
<message id="Lms.Explore.mixer">混音师</message> <message id="Lms.Explore.mixer">混音师</message>
<message id="Lms.Explore.mood">情绪</message>
<message id="Lms.Explore.most-played">播放最多</message> <message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
@@ -316,6 +327,22 @@
<message id="Lms.Settings.backend.internal">内部</message> <message id="Lms.Settings.backend.internal">内部</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API 令牌</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API 令牌</message>
<message id="Lms.Settings.backend.lastfm">Last.fm</message>
<message id="Lms.Settings.services.lastfm-api-key">Last.fm API 密钥</message>
<message id="Lms.Settings.services.lastfm-api-secret">Last.fm API 共享密钥</message>
<message id="Lms.Settings.services.lastfm-card-title">Last.fm 账户</message>
<message id="Lms.Settings.services.lastfm-linked">已关联</message>
<message id="Lms.Settings.services.lastfm-link">关联账户</message>
<message id="Lms.Settings.services.lastfm-unlink">取消关联账户</message>
<message id="Lms.Settings.services.lastfm-link-modal-title">关联 Last.fm 账户</message>
<message id="Lms.Settings.services.lastfm-link-modal-help">输入您的 Last.fm API 密钥和共享密钥(从 <a href="https://www.last.fm/api/account/create" target="_blank">last.fm/api/account/create</a> 获取),然后点击授权。</message>
<message id="Lms.Settings.services.lastfm-authorize">在 Last.fm 上授权</message>
<message id="Lms.Settings.services.lastfm-done">完成,我已授权</message>
<message id="Lms.Settings.services.lastfm-auth-url">未打开?点击此处在 Last.fm 上授权</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-title">取消关联 Last.fm 账户</message>
<message id="Lms.Settings.services.lastfm-unlink-confirm-body">您确定要取消关联 Last.fm 账户吗?在重新关联之前,scrobbling 将停止。</message>
<message id="Lms.Settings.services.lastfm-missing-credentials">请输入您的 Last.fm API 密钥和共享密钥。</message>
<message id="Lms.Settings.services.lastfm-auth-error">Last.fm 认证失败。请检查您的 API 密钥和共享密钥。</message>
<message id="Lms.Settings.change-password">更改密码</message> <message id="Lms.Settings.change-password">更改密码</message>
<message id="Lms.Settings.date-asc">发行日期(升序)</message> <message id="Lms.Settings.date-asc">发行日期(升序)</message>
<message id="Lms.Settings.date-desc">发行日期(降序)</message> <message id="Lms.Settings.date-desc">发行日期(降序)</message>
+60
View File
@@ -39,4 +39,64 @@
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
</message> </message>
<message id="Lms.Settings.services.lastfm.template.card">
<div class="card mt-3">
<div class="card-header d-flex align-items-center gap-2">
${tr:Lms.Settings.services.lastfm-card-title}
${card-header-badge}
</div>
<div class="card-body d-flex align-items-center gap-2">
${card-content}
</div>
</div>
</message>
<message id="Lms.Settings.services.lastfm.template.link-modal">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Settings.services.lastfm-link-modal-title}</h5>
</div>
<div class="modal-body">
<p>${tr:Lms.Settings.services.lastfm-link-modal-help}</p>
<div class="mb-3">
<label class="form-label">${tr:Lms.Settings.services.lastfm-api-key}</label>
${api-key class="form-control"}
</div>
<div class="mb-3">
<label class="form-label">${tr:Lms.Settings.services.lastfm-api-secret}</label>
<div class="input-group">
${api-secret class="form-control"}
${api-secret-visibility-btn class="btn btn-outline-secondary"}
</div>
</div>
${auth-anchor-container}
</div>
<div class="modal-footer">
${authorize-btn class="btn btn-primary me-1"}${done-btn class="btn btn-success me-1"}${cancel-btn class="btn btn-secondary"}
</div>
</div>
</div>
</div>
</message>
<message id="Lms.Settings.services.lastfm.template.unlink-confirm">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Settings.services.lastfm-unlink-confirm-title}</h5>
</div>
<div class="modal-body">
${tr:Lms.Settings.services.lastfm-unlink-confirm-body}
</div>
<div class="modal-footer">
${confirm-btn class="btn btn-danger me-1"}${cancel-btn class="btn btn-secondary"}
</div>
</div>
</div>
</div>
</message>
</messages> </messages>
+40
View File
@@ -80,6 +80,46 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="container-fluid d-grid gap-2"> <div class="container-fluid d-grid gap-2">
${<if-has-genres>}
<div class="row align-items-center">
<div class="col-4">
${genres-label}
</div>
<div class="col">
${genres}
</div>
</div>
${</if-has-genres>}
${<if-has-groupings>}
<div class="row align-items-center">
<div class="col-4">
${groupings-label}
</div>
<div class="col">
${groupings}
</div>
</div>
${</if-has-groupings>}
${<if-has-languages>}
<div class="row align-items-center">
<div class="col-4">
${languages-label}
</div>
<div class="col">
${languages}
</div>
</div>
${</if-has-languages>}
${<if-has-moods>}
<div class="row align-items-center">
<div class="col-4">
${moods-label}
</div>
<div class="col">
${moods}
</div>
</div>
${</if-has-moods>}
${<if-has-clusters>} ${<if-has-clusters>}
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-4"> <div class="col-4">
+6
View File
@@ -61,6 +61,12 @@ listenbrainz-max-sync-feedback-count = 1000;
# How often to resync feedbacks (0 to disable sync) # How often to resync feedbacks (0 to disable sync)
listenbrainz-sync-feedbacks-period-hours = 1; listenbrainz-sync-feedbacks-period-hours = 1;
# Last.fm API and authorization URLs
lastfm-api-base-url = "https://ws.audioscrobbler.com";
lastfm-auth-base-url = "https://www.last.fm";
# How often to submit pending scrobbles (0 to disable)
lastfm-submit-period-hours = 1;
# Acousticbrainz root API # Acousticbrainz root API
acousticbrainz-api-base-url = "https://acousticbrainz.org"; acousticbrainz-api-base-url = "https://acousticbrainz.org";
+105 -59
View File
@@ -67,6 +67,37 @@ namespace lms::audio::ffmpeg
throw Exception("Unsupported PcmSampleType"); throw Exception("Unsupported PcmSampleType");
} }
SwrContextPtr createResampler(const PcmParameters& params, const AVChannelLayout& inLayout, AVSampleFormat inFmt, int inSampleRate)
{
const ::AVSampleFormat outFmt{ toAvSampleFormat(params.sampleType, params.planar) };
AVChannelLayout outLayout;
::av_channel_layout_default(&outLayout, static_cast<int>(params.channelCount));
::SwrContext* context{};
const int err{ ::swr_alloc_set_opts2(
&context,
&outLayout,
outFmt,
static_cast<int>(params.sampleRate),
&inLayout,
inFmt,
inSampleRate,
0,
nullptr) };
::av_channel_layout_uninit(&outLayout);
if (err < 0 || !context)
throw FFmpegException{ "Cannot allocate resampler context", err };
SwrContextPtr resampleContext{ context };
const int initErr{ ::swr_init(resampleContext.get()) };
if (initErr < 0)
throw FFmpegException{ "Cannot initialize resampler", initErr };
return resampleContext;
}
} // namespace } // namespace
PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters) PcmDecoder::PcmDecoder(const std::filesystem::path& filePath, std::chrono::microseconds offset, const PcmParameters& parameters)
@@ -159,36 +190,14 @@ namespace lms::audio::ffmpeg
if (!_inputPacket) if (!_inputPacket)
throw Exception{ "Cannot allocate input packet" }; throw Exception{ "Cannot allocate input packet" };
// Resampler _resampleContext = createResampler(_parameters, _decoderContext->ch_layout, _decoderContext->sample_fmt, _decoderContext->sample_rate);
const ::AVSampleFormat outFmt{ toAvSampleFormat(_parameters.sampleType, _parameters.planar) }; _resamplerInputConfig = {
AVChannelLayout outLayout; _decoderContext->sample_rate,
::av_channel_layout_default(&outLayout, _parameters.channelCount); static_cast<int>(_decoderContext->sample_fmt),
_decoderContext->ch_layout.nb_channels,
{ static_cast<int>(_decoderContext->ch_layout.order),
::SwrContext* context{}; _decoderContext->ch_layout.u.mask,
::swr_alloc_set_opts2( };
&context, // existing context
&outLayout, // out layout
outFmt, // out format
static_cast<int>(_parameters.sampleRate), // out rate
&_decoderContext->ch_layout, // in layout
_decoderContext->sample_fmt, // in format
_decoderContext->sample_rate, // in rate
0, // log offset
nullptr);
::av_channel_layout_uninit(&outLayout);
if (!context)
throw Exception{ "Cannot allocate resampler context" };
_resampleContext = SwrContextPtr{ context };
}
{
int error{ ::swr_init(_resampleContext.get()) };
if (error < 0)
throw FFmpegException{ "Cannot initialize resampler", error };
}
} }
PcmDecoder::~PcmDecoder() = default; PcmDecoder::~PcmDecoder() = default;
@@ -232,34 +241,28 @@ namespace lms::audio::ffmpeg
} }
else else
{ {
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{}; const std::size_t outSampleCount{ resampleFrame(outputChannelBuffers, maxSamplesPerChannel, _decodedFrame.get()) };
for (std::size_t i{}; i < outputChannelBuffers.size(); ++i)
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data());
// Resample decoded audio
const int outSampleCount{ ::swr_convert(
_resampleContext.get(),
outData.data(),
static_cast<int>(maxSamplesPerChannel),
(const uint8_t**)_decodedFrame->data,
_decodedFrame->nb_samples) };
::av_frame_unref(_decodedFrame.get()); ::av_frame_unref(_decodedFrame.get());
if (outSampleCount < 0)
throw FFmpegException{ "swr_convert failed", outSampleCount };
if (outSampleCount > 0) if (outSampleCount > 0)
return static_cast<std::size_t>(outSampleCount); return outSampleCount;
continue; // Rare but legal: frame produced no output (delay accumulation) continue; // delay accumulation: resampler is buffering, no output yet
} }
// Drain resampler once decoder is drained // Drain resampler once decoder is drained
if (_draining) if (_draining)
{
try
{ {
const std::size_t outSampleCount{ drainResampler(outputChannelBuffers, maxSamplesPerChannel) }; const std::size_t outSampleCount{ drainResampler(outputChannelBuffers, maxSamplesPerChannel) };
if (outSampleCount > 0) if (outSampleCount > 0)
return outSampleCount; return outSampleCount;
}
catch (const Exception& e)
{
LMS_LOG(AUDIO, ERROR, "Failed to drain resampler: " << e.what());
}
_finished = true; _finished = true;
break; break;
@@ -286,7 +289,6 @@ namespace lms::audio::ffmpeg
if (outputChannelBuffers.size() != _parameters.channelCount) if (outputChannelBuffers.size() != _parameters.channelCount)
throw Exception{ "Expected " + std::to_string(_parameters.channelCount) + " buffers for planar output" }; throw Exception{ "Expected " + std::to_string(_parameters.channelCount) + " buffers for planar output" };
// Each planar buffer holds samples for one channel only
const int bytesPerSample{ av_get_bytes_per_sample(toAvSampleFormat(_parameters.sampleType, true)) }; const int bytesPerSample{ av_get_bytes_per_sample(toAvSampleFormat(_parameters.sampleType, true)) };
if (bytesPerSample <= 0) if (bytesPerSample <= 0)
throw Exception{ "Invalid bytes per sample for output format" }; throw Exception{ "Invalid bytes per sample for output format" };
@@ -306,7 +308,6 @@ namespace lms::audio::ffmpeg
if (bytesPerSample <= 0) if (bytesPerSample <= 0)
throw Exception{ "Invalid bytes per sample for output format" }; throw Exception{ "Invalid bytes per sample for output format" };
// Divide by (bytes per sample * number of channels) for interleaved
const std::size_t sampleCount = outputChannelBuffers[0].size() / (bytesPerSample * _parameters.channelCount); const std::size_t sampleCount = outputChannelBuffers[0].size() / (bytesPerSample * _parameters.channelCount);
return sampleCount; return sampleCount;
@@ -353,28 +354,73 @@ namespace lms::audio::ffmpeg
} }
} }
std::size_t PcmDecoder::drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel) bool PcmDecoder::inputFormatChanged(const AVFrame* frame) const
{ {
std::array<uint8_t*, AV_NUM_DATA_POINTERS> outData{}; const auto& c{ _resamplerInputConfig };
for (std::size_t i{}; i < outputChannelBuffers.size(); ++i) if (frame->sample_rate != c.sampleRate
outData[i] = reinterpret_cast<uint8_t*>(outputChannelBuffers[i].data()); || frame->format != c.sampleFormat
|| frame->ch_layout.nb_channels != c.nbChannels
|| static_cast<int>(frame->ch_layout.order) != c.channelOrder)
return true;
if (frame->ch_layout.order == AV_CHANNEL_ORDER_NATIVE)
return frame->ch_layout.u.mask != c.channelMask;
return false;
}
const int outSampleCount{ ::swr_convert(_resampleContext.get(), void PcmDecoder::reinitResamplerForFrame(const AVFrame* frame)
{
// The format change is caused by a corrupt/non-standard frame so the lost samples are likely garbled audio anyway.
_resampleContext = createResampler(_parameters, frame->ch_layout, static_cast<AVSampleFormat>(frame->format), frame->sample_rate);
_resamplerInputConfig = {
frame->sample_rate,
frame->format,
frame->ch_layout.nb_channels,
static_cast<int>(frame->ch_layout.order),
frame->ch_layout.u.mask,
};
}
std::size_t PcmDecoder::resampleFrame(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel, const AVFrame* inputFrame)
{
if (inputFrame && inputFormatChanged(inputFrame))
{
LMS_LOG(AUDIO, DEBUG, "Input format changed");
reinitResamplerForFrame(inputFrame);
}
std::array<std::uint8_t*, AV_NUM_DATA_POINTERS> outData{};
for (std::size_t i{}; i < outputChannelBuffers.size(); ++i)
outData[i] = static_cast<std::uint8_t*>(static_cast<void*>(outputChannelBuffers[i].data()));
std::array<const std::uint8_t*, AV_NUM_DATA_POINTERS> inData{};
if (inputFrame)
{
for (int i{}; i < AV_NUM_DATA_POINTERS; ++i)
inData[i] = inputFrame->data[i];
}
const int outSampleCount{ ::swr_convert(
_resampleContext.get(),
outData.data(), outData.data(),
static_cast<int>(maxSamplesPerChannel), static_cast<int>(maxSamplesPerChannel),
nullptr, inputFrame ? inData.data() : nullptr,
0) }; inputFrame ? inputFrame->nb_samples : 0) };
if (outSampleCount < 0) if (outSampleCount < 0)
throw FFmpegException{ "swr_convert (drain) failed", outSampleCount }; throw FFmpegException{ inputFrame ? "swr_convert failed" : "swr_convert (drain) failed", outSampleCount };
return outSampleCount; return static_cast<std::size_t>(outSampleCount);
}
std::size_t PcmDecoder::drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel)
{
return resampleFrame(outputChannelBuffers, maxSamplesPerChannel, nullptr);
} }
std::size_t PcmDecoder::getEstimatedResamplerAvailableSamples() const std::size_t PcmDecoder::getEstimatedResamplerAvailableSamples() const
{ {
const int64_t delayedInputSampleCount{ ::swr_get_delay(_resampleContext.get(), _decoderContext->sample_rate) }; const int64_t delayedInputSampleCount{ ::swr_get_delay(_resampleContext.get(), _resamplerInputConfig.sampleRate) };
const int64_t sampleCount{ av_rescale_rnd(delayedInputSampleCount, _parameters.sampleRate, _decoderContext->sample_rate, AV_ROUND_UP) }; const int64_t sampleCount{ av_rescale_rnd(delayedInputSampleCount, _parameters.sampleRate, _resamplerInputConfig.sampleRate, AV_ROUND_UP) };
return sampleCount; return sampleCount;
} }
+13
View File
@@ -44,6 +44,9 @@ namespace lms::audio::ffmpeg
std::size_t computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const; std::size_t computeSampleCountPerChannel(std::span<WritableBuffer> outputChannelBuffers) const;
void feedDecoder(); void feedDecoder();
bool inputFormatChanged(const AVFrame* frame) const;
void reinitResamplerForFrame(const AVFrame* frame);
std::size_t resampleFrame(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel, const AVFrame* inputFrame);
std::size_t drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel); std::size_t drainResampler(std::span<WritableBuffer> outputChannelBuffers, std::size_t maxSamplesPerChannel);
std::size_t getEstimatedResamplerAvailableSamples() const; std::size_t getEstimatedResamplerAvailableSamples() const;
@@ -60,5 +63,15 @@ namespace lms::audio::ffmpeg
AVFramePtr _decodedFrame; AVFramePtr _decodedFrame;
AVPacketPtr _inputPacket; AVPacketPtr _inputPacket;
SwrContextPtr _resampleContext; SwrContextPtr _resampleContext;
struct ResamplerInputConfig
{
int sampleRate{};
int sampleFormat{};
int nbChannels{};
int channelOrder{};
std::uint64_t channelMask{};
};
ResamplerInputConfig _resamplerInputConfig;
}; };
} // namespace lms::audio::ffmpeg } // namespace lms::audio::ffmpeg
@@ -32,7 +32,7 @@ namespace lms::audio
namespace namespace
{ {
constexpr uint32_t byteswap32(uint32_t x) constexpr std::uint32_t byteswap32(uint32_t x)
{ {
return (x >> 24) | ((x >> 8) & 0x0000FF00u) | ((x << 8) & 0x00FF0000u) | (x << 24); return (x >> 24) | ((x >> 8) & 0x0000FF00u) | ((x << 8) & 0x00FF0000u) | (x << 24);
} }
@@ -44,7 +44,7 @@ namespace lms::audio
for (std::size_t i{}; i < data.size(); ++i) for (std::size_t i{}; i < data.size(); ++i)
{ {
uint32_t bits{ std::bit_cast<uint32_t>(data[i]) }; std::uint32_t bits{ std::bit_cast<std::uint32_t>(data[i]) };
if constexpr (std::endian::native == std::endian::little) if constexpr (std::endian::native == std::endian::little)
bits = byteswap32(bits); bits = byteswap32(bits);
std::memcpy(blob.data() + i * 4, &bits, 4); std::memcpy(blob.data() + i * 4, &bits, 4);
@@ -58,7 +58,7 @@ namespace lms::audio
for (std::size_t i{}; i < data.size(); ++i) for (std::size_t i{}; i < data.size(); ++i)
{ {
uint32_t bits{}; std::uint32_t bits{};
std::memcpy(&bits, blob.data() + i * 4, 4); std::memcpy(&bits, blob.data() + i * 4, 4);
if constexpr (std::endian::native == std::endian::little) if constexpr (std::endian::native == std::endian::little)
bits = byteswap32(bits); bits = byteswap32(bits);
+1
View File
@@ -34,6 +34,7 @@ add_library(lmscore STATIC
impl/String.cpp impl/String.cpp
impl/TraceLogger.cpp impl/TraceLogger.cpp
impl/UUID.cpp impl/UUID.cpp
impl/Md5.cpp
impl/XxHash3.cpp impl/XxHash3.cpp
${CMAKE_CURRENT_BINARY_DIR}/impl/Version.cpp ${CMAKE_CURRENT_BINARY_DIR}/impl/Version.cpp
) )
+1
View File
@@ -2,6 +2,7 @@
add_executable(bench-core add_executable(bench-core
Core.cpp Core.cpp
TraceLoggerBench.cpp TraceLoggerBench.cpp
UUIDBench.cpp
) )
target_link_libraries(bench-core PRIVATE target_link_libraries(bench-core PRIVATE
+55
View File
@@ -0,0 +1,55 @@
/*
* 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 <benchmark/benchmark.h>
#include "core/UUID.hpp"
namespace lms::core::benchs
{
static void BM_UUID_fromString(benchmark::State& state)
{
for (auto _ : state)
benchmark::DoNotOptimize(UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fc"));
}
static void BM_UUID_fromString_invalid(benchmark::State& state)
{
for (auto _ : state)
benchmark::DoNotOptimize(UUID::fromString("not-a-valid-uuid-string-at-all-xx"));
}
static void BM_UUID_toString(benchmark::State& state)
{
const UUID uuid{ *UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fc") };
for (auto _ : state)
benchmark::DoNotOptimize(uuid.toString());
}
static void BM_UUID_generate(benchmark::State& state)
{
for (auto _ : state)
benchmark::DoNotOptimize(UUID::generate());
}
BENCHMARK(BM_UUID_fromString);
BENCHMARK(BM_UUID_fromString_invalid);
BENCHMARK(BM_UUID_toString);
BENCHMARK(BM_UUID_generate);
} // namespace lms::core::benchs
+36 -5
View File
@@ -19,15 +19,47 @@
#include "Logger.hpp" #include "Logger.hpp"
#include <Wt/WDateTime.h>
#include <algorithm> #include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <string_view>
#include <thread> #include <thread>
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/String.hpp"
namespace
{
std::string localISO8601TimeString()
{
using Clock = std::chrono::system_clock;
const auto now{ Clock::now() };
const int ms{ static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000) };
const std::time_t tt{ Clock::to_time_t(now) };
std::tm tm{};
::localtime_r(&tt, &tm);
std::array<char, 24> datetime{};
std::array<char, 8> offset{};
std::strftime(datetime.data(), datetime.size(), "%Y-%m-%dT%H:%M:%S", &tm);
std::strftime(offset.data(), offset.size(), "%z", &tm); // "+HHMM" or "-HHMM"
// Append msecs and offset
const std::string_view offsetView{ offset.data() };
std::string result{ datetime.data() };
result += '.';
result += static_cast<char>('0' + ms / 100);
result += static_cast<char>('0' + (ms / 10) % 10);
result += static_cast<char>('0' + ms % 10);
result += offsetView.substr(0, 3); // sign + HH
result += ':';
result += offsetView.substr(3); // MM
return result;
}
} // namespace
namespace lms::core::logging namespace lms::core::logging
{ {
@@ -205,9 +237,8 @@ namespace lms::core::logging
{ {
assert(isSeverityActive(severity)); // should have been filtered out by a isSeverityActive call assert(isSeverityActive(severity)); // should have been filtered out by a isSeverityActive call
OutputStream* outputStream{ _severityToOutputStreamMap.at(severity) }; OutputStream* outputStream{ _severityToOutputStreamMap.at(severity) };
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
std::unique_lock lock{ outputStream->mutex }; std::unique_lock lock{ outputStream->mutex };
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl; outputStream->stream << localISO8601TimeString() << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
} }
} // namespace lms::core::logging } // namespace lms::core::logging
+35
View File
@@ -0,0 +1,35 @@
/*
* 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/Md5.hpp"
#include <Wt/Utils.h>
namespace lms::core
{
std::array<std::byte, 16> md5(std::string_view data)
{
const std::string raw{ Wt::Utils::md5(std::string{ data }) };
std::array<std::byte, 16> result;
for (std::size_t i{}; i < 16; ++i)
result[i] = static_cast<std::byte>(static_cast<unsigned char>(raw[i]));
return result;
}
} // namespace lms::core
+16
View File
@@ -462,6 +462,22 @@ namespace lms::core::stringUtils
detail::writeEscapedString(os, str, detail::xmlEscapeChars); detail::writeEscapedString(os, str, detail::xmlEscapeChars);
} }
std::string urlEncode(std::string_view str)
{
std::ostringstream encoded;
encoded << std::hex << std::uppercase;
for (const unsigned char c : str)
{
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
encoded << c;
else
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(c);
}
return encoded.str();
}
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
{ {
std::string res; std::string res;
+59 -38
View File
@@ -19,72 +19,93 @@
#include "core/UUID.hpp" #include "core/UUID.hpp"
#include <cassert> #include <array>
#include <iomanip> #include <charconv>
#include <regex> #include <cstddef>
#include <sstream> #include <cstdint>
#include "core/Random.hpp" #include "core/Random.hpp"
#include "core/String.hpp"
namespace lms::core namespace lms::core
{ {
namespace stringUtils namespace stringUtils
{ {
template<> template<>
std::optional<UUID> std::optional<UUID> readAs(std::string_view str)
readAs(std::string_view str)
{ {
return UUID::fromString(str); return UUID::fromString(str);
} }
} // namespace stringUtils } // namespace stringUtils
namespace namespace
{ {
bool stringIsUUID(std::string_view str) // Each entry is the str index of the high hex char for that UUID byte
{ constexpr std::array<std::size_t, 16> byteOffsets{
static const std::regex re{ R"([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12})" }; 0, 2, 4, 6, // group 1 (4 bytes, positions 0-7)
9, 11, // group 2 (2 bytes, positions 9-12)
14, 16, // group 3 (2 bytes, positions 14-17)
19, 21, // group 4 (2 bytes, positions 19-22)
24, 26, 28, 30, 32, 34 // group 5 (6 bytes, positions 24-35)
};
return std::regex_match(std::cbegin(str), std::cend(str), re); bool parseUUID(std::string_view str, std::array<std::byte, 16>& out)
{
if (str.size() != 36 || str[8] != '-' || str[13] != '-' || str[18] != '-' || str[23] != '-')
return false;
for (std::size_t i{}; i < 16; ++i)
{
unsigned int byte{};
const char* begin{ str.data() + byteOffsets[i] };
const auto [ptr, ec]{ std::from_chars(begin, begin + 2, byte, 16) };
if (ec != std::errc{} || ptr != begin + 2)
return false;
out[i] = static_cast<std::byte>(byte);
}
return true;
} }
} // namespace } // namespace
UUID::UUID(std::string_view str) UUID::UUID(std::array<std::byte, 16> bytes) noexcept
: _value{ stringUtils::stringToLower(str) } : _bytes{ bytes }
{ {
} }
std::optional<UUID> UUID::fromString(std::string_view str) std::optional<UUID> UUID::fromString(std::string_view str)
{ {
if (!stringIsUUID(str)) std::array<std::byte, 16> bytes{};
if (!parseUUID(str, bytes))
return std::nullopt; return std::nullopt;
return UUID{ bytes };
}
return UUID{ str }; UUID UUID::fromBytes(std::span<const std::byte, 16> bytes) noexcept
{
std::array<std::byte, 16> arr{};
std::copy(bytes.begin(), bytes.end(), arr.begin());
return UUID{ arr };
}
std::string UUID::toString() const
{
static constexpr char hex[]{ "0123456789abcdef" };
std::string s(36, '-');
for (std::size_t i{}; i < 16; ++i)
{
const auto b{ std::to_integer<unsigned char>(_bytes[i]) };
s[byteOffsets[i]] = hex[b >> 4];
s[byteOffsets[i] + 1] = hex[b & 0x0F];
}
return s;
} }
UUID UUID::generate() UUID UUID::generate()
{ {
// Form is "123e4567-e89b-12d3-a456-426614174000" std::uniform_int_distribution<std::uint8_t> dist{ 0, 255 };
// TODO: store 128 bits and only convert to string when necessary auto& rng{ random::getRandGenerator() };
std::array<std::byte, binarySize> bytes{};
std::ostringstream oss; for (auto& b : bytes)
b = static_cast<std::byte>(dist(rng));
auto concatRandomBytes{ [](std::ostream& os, std::size_t byteCount) { return UUID{ bytes };
for (std::size_t i{}; i < byteCount; ++i)
os << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(random::getRandom<std::uint8_t>(0, 255));
} };
concatRandomBytes(oss, 4);
oss << "-";
concatRandomBytes(oss, 2);
oss << "-";
concatRandomBytes(oss, 2);
oss << "-";
concatRandomBytes(oss, 2);
oss << "-";
concatRandomBytes(oss, 6);
const auto uuid{ fromString(oss.str()) };
assert(uuid);
return uuid.value();
} }
} // namespace lms::core } // namespace lms::core
+6 -2
View File
@@ -284,8 +284,12 @@ namespace lms::core::http
LOG(DEBUG, "Remaining messages = " << (remainingCount ? *remainingCount : 0)); LOG(DEBUG, "Remaining messages = " << (remainingCount ? *remainingCount : 0));
if (mustThrottle || (remainingCount && *remainingCount == 0)) if (mustThrottle || (remainingCount && *remainingCount == 0))
{ {
const auto waitDuration{ headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In") }; const std::chrono::seconds waitDuration{
throttle(waitDuration.value_or(_defaultRetryWaitDuration)); headerReadAs<std::chrono::seconds>(msg, "X-RateLimit-Reset-In")
.value_or(headerReadAs<std::chrono::seconds>(msg, "Retry-After")
.value_or(_defaultRetryWaitDuration))
};
throttle(waitDuration);
} }
if (!mustThrottle) if (!mustThrottle)
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 Emeric Poupon * Copyright (C) 2025 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,25 +19,11 @@
#pragma once #pragma once
#include <ctime> #include <array>
#include <cstddef>
#include <string_view>
#include "core/String.hpp" namespace lms::core
#include "database/objects/ArtworkId.hpp"
namespace lms::api::subsonic
{ {
struct CoverArtId std::array<std::byte, 16> md5(std::string_view data);
{ } // namespace lms::core
db::ArtworkId id;
std::time_t timestamp;
};
std::string idToString(CoverArtId coverId);
} // namespace lms::api::subsonic
// Used to parse parameters
namespace lms::core::stringUtils
{
template<>
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str);
} // namespace lms::core::stringUtils
+2
View File
@@ -108,6 +108,8 @@ namespace lms::core::stringUtils
[[nodiscard]] std::string xmlEscape(std::string_view str); [[nodiscard]] std::string xmlEscape(std::string_view str);
void writeXmlEscapedString(std::ostream& os, std::string_view str); void writeXmlEscapedString(std::ostream& os, std::string_view str);
[[nodiscard]] std::string urlEncode(std::string_view str);
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); [[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
[[nodiscard]] std::string unescapeString(std::string_view str, char escapeChar); [[nodiscard]] std::string unescapeString(std::string_view str, char escapeChar);
+15 -7
View File
@@ -19,8 +19,11 @@
#pragma once #pragma once
#include <array>
#include <cstddef>
#include <functional> #include <functional>
#include <optional> #include <optional>
#include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -31,24 +34,28 @@ namespace lms::core
class UUID class UUID
{ {
public: public:
static constexpr std::size_t binarySize{ 16 };
UUID() noexcept = default;
static std::optional<UUID> fromString(std::string_view str); static std::optional<UUID> fromString(std::string_view str);
static UUID fromBytes(std::span<const std::byte, binarySize> bytes) noexcept;
static UUID generate(); static UUID generate();
std::string_view getAsString() const { return _value; } std::string toString() const;
std::span<const std::byte, binarySize> bytes() const noexcept { return _bytes; }
auto operator<=>(const UUID&) const = default; auto operator<=>(const UUID&) const = default;
private: private:
UUID(std::string_view value); explicit UUID(std::array<std::byte, binarySize> bytes) noexcept;
std::string _value; std::array<std::byte, binarySize> _bytes{};
}; };
} // namespace lms::core } // namespace lms::core
namespace lms::core::stringUtils namespace lms::core::stringUtils
{ {
template<> template<>
std::optional<UUID> std::optional<UUID> readAs(std::string_view str);
readAs(std::string_view str);
} }
namespace std namespace std
@@ -56,9 +63,10 @@ namespace std
template<> template<>
struct hash<lms::core::UUID> struct hash<lms::core::UUID>
{ {
size_t operator()(const lms::core::UUID& str) const size_t operator()(const lms::core::UUID& uuid) const noexcept
{ {
return hash<std::string_view>{}(str.getAsString()); const auto& b{ uuid.bytes() };
return hash<string_view>{}({ static_cast<const char*>(static_cast<const void*>(b.data())), b.size() });
} }
}; };
} // namespace std } // namespace std
+61 -4
View File
@@ -17,10 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <atomic> #include <unordered_set>
#include <chrono>
#include <thread>
#include <vector>
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -28,6 +25,19 @@
namespace lms::core namespace lms::core
{ {
TEST(UUID, fromString_invalid)
{
EXPECT_FALSE(UUID::fromString(""));
EXPECT_FALSE(UUID::fromString("not-a-uuid"));
EXPECT_FALSE(UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178f")); // too short
EXPECT_FALSE(UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fcc")); // too long
EXPECT_FALSE(UUID::fromString("3f51c839-bee2-4e9d-a7b7_0693e45178fc")); // wrong separator position 23
EXPECT_FALSE(UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178gz")); // invalid hex char 'g','z'
EXPECT_FALSE(UUID::fromString("3f51c839Xbee2-4e9d-a7b7-0693e45178fc")); // dash replaced at position 8
EXPECT_FALSE(UUID::fromString("3f51c839-bee2X4e9d-a7b7-0693e45178fc")); // dash replaced at position 13
EXPECT_FALSE(UUID::fromString("3f51c839-bee2-4e9dXa7b7-0693e45178fc")); // dash replaced at position 18
}
TEST(UUID, caseInsensitive) TEST(UUID, caseInsensitive)
{ {
const std::optional<UUID> uuid1{ UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fc") }; const std::optional<UUID> uuid1{ UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fc") };
@@ -37,4 +47,51 @@ namespace lms::core
EXPECT_TRUE(uuid1 >= uuid2); EXPECT_TRUE(uuid1 >= uuid2);
EXPECT_TRUE(uuid1 <= uuid2); EXPECT_TRUE(uuid1 <= uuid2);
} }
TEST(UUID, toString_roundTrip)
{
const std::string str{ "3f51c839-bee2-4e9d-a7b7-0693e45178fc" };
const std::optional<UUID> uuid{ UUID::fromString(str) };
ASSERT_TRUE(uuid);
EXPECT_EQ(uuid->toString(), str);
}
TEST(UUID, toString_lowercase)
{
const std::optional<UUID> uuid{ UUID::fromString("3F51C839-BEE2-4E9D-A7B7-0693E45178FC") };
ASSERT_TRUE(uuid);
EXPECT_EQ(uuid->toString(), "3f51c839-bee2-4e9d-a7b7-0693e45178fc");
}
TEST(UUID, fromBytes_roundTrip)
{
const std::optional<UUID> uuid{ UUID::fromString("550e8400-e29b-41d4-a716-446655440000") };
ASSERT_TRUE(uuid);
const UUID fromB{ UUID::fromBytes(uuid->bytes()) };
EXPECT_EQ(fromB, *uuid);
EXPECT_EQ(fromB.toString(), "550e8400-e29b-41d4-a716-446655440000");
}
TEST(UUID, bytes_size)
{
const std::optional<UUID> uuid{ UUID::fromString("3f51c839-bee2-4e9d-a7b7-0693e45178fc") };
ASSERT_TRUE(uuid);
EXPECT_EQ(uuid->bytes().size(), 16U);
}
TEST(UUID, generate_validString)
{
const UUID uuid{ UUID::generate() };
const std::string s{ uuid.toString() };
ASSERT_EQ(s.size(), 36U);
EXPECT_EQ(s[8], '-');
EXPECT_EQ(s[13], '-');
EXPECT_EQ(s[18], '-');
EXPECT_EQ(s[23], '-');
EXPECT_TRUE(UUID::fromString(s).has_value());
}
} // namespace lms::core } // namespace lms::core
+4
View File
@@ -6,10 +6,14 @@ add_library(lmsdatabase STATIC
impl/objects/AuthToken.cpp impl/objects/AuthToken.cpp
impl/objects/Cluster.cpp impl/objects/Cluster.cpp
impl/objects/Directory.cpp impl/objects/Directory.cpp
impl/objects/Genre.cpp
impl/objects/Grouping.cpp
impl/objects/Image.cpp impl/objects/Image.cpp
impl/objects/Language.cpp
impl/objects/Listen.cpp impl/objects/Listen.cpp
impl/objects/MediaLibrary.cpp impl/objects/MediaLibrary.cpp
impl/objects/Medium.cpp impl/objects/Medium.cpp
impl/objects/Mood.cpp
impl/objects/PlayListFile.cpp impl/objects/PlayListFile.cpp
impl/objects/PlayQueue.cpp impl/objects/PlayQueue.cpp
impl/objects/Podcast.cpp impl/objects/Podcast.cpp
+28
View File
@@ -170,6 +170,24 @@ namespace lms::db
return res; return res;
} }
std::optional<long long> getMmapSize(Wt::Dbo::SqlConnection& connection)
{
auto statement = connection.prepareStatement("PRAGMA mmap_size");
statement->execute();
std::optional<long long> res;
while (statement->nextRow())
{
assert(!res);
long long value{};
if (statement->getResult(0, &value))
res = value;
break;
}
return res;
}
void getCompileOptions(Wt::Dbo::SqlConnection& connection, std::function<void(std::string_view compileOption)> callback) void getCompileOptions(Wt::Dbo::SqlConnection& connection, std::function<void(std::string_view compileOption)> callback)
{ {
auto statement = connection.prepareStatement("PRAGMA compile_options"); auto statement = connection.prepareStatement("PRAGMA compile_options");
@@ -217,6 +235,7 @@ namespace lms::db
logPageSize(); logPageSize();
logCacheSize(); logCacheSize();
logMmapSize();
logCompileOptions(); logCompileOptions();
if (checkType == "quick") if (checkType == "quick")
{ {
@@ -278,6 +297,15 @@ namespace lms::db
LMS_LOG(DB, INFO, "Cache size set to " << *cacheSize); LMS_LOG(DB, INFO, "Cache size set to " << *cacheSize);
} }
void Db::logMmapSize()
{
ScopedConnection connection{ *_connectionPool };
const std::optional<long long> mmapSize{ getMmapSize(*connection) };
if (mmapSize)
LMS_LOG(DB, INFO, "Mmap size set to " << *mmapSize);
}
void Db::logCompileOptions() void Db::logCompileOptions()
{ {
ScopedConnection connection{ *_connectionPool }; ScopedConnection connection{ *_connectionPool };
+1
View File
@@ -51,6 +51,7 @@ namespace lms::db
void logPageSize(); void logPageSize();
void logCacheSize(); void logCacheSize();
void logMmapSize();
void logCompileOptions(); void logCompileOptions();
void performQuickCheck(); void performQuickCheck();
void performIntegrityCheck(); void performIntegrityCheck();
+207 -8
View File
@@ -24,6 +24,7 @@
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
@@ -35,7 +36,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 105 }; static constexpr Version LMS_DATABASE_VERSION{ 108 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -90,10 +91,14 @@ namespace lms::db::Migration
{ {
void dropIndexes(Session& session) void dropIndexes(Session& session)
{ {
LMS_LOG(DB, INFO, "Droping all indexes...");
// Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) };
for (const auto& indexName : indexeNames) for (const auto& indexName : indexeNames)
utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName); utils::executeCommand(*session.getDboSession(), "DROP INDEX " + indexName);
LMS_LOG(DB, INFO, "Indexes dropped!");
} }
void migrateFromV33(Session& session) void migrateFromV33(Session& session)
@@ -1729,6 +1734,194 @@ FROM track)");
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE "playlist_file" ADD COLUMN "cover_image_file" text NOT NULL DEFAULT '')"); utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE "playlist_file" ADD COLUMN "cover_image_file" text NOT NULL DEFAULT '')");
} }
void migrateFromV105(Session& session)
{
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE "user" ADD COLUMN "lastfm_api_key" TEXT NOT NULL DEFAULT '')");
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE "user" ADD COLUMN "lastfm_api_secret" TEXT NOT NULL DEFAULT '')");
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE "user" ADD COLUMN "lastfm_session_key" TEXT NOT NULL DEFAULT '')");
}
void migrateFromV106(Session& session)
{
auto& dboSession{ *session.getDboSession() };
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS artist_name_mbid_idx");
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS artist_mbid_idx");
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS release_mbid_idx");
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS release_group_mbid_idx");
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS track_mbid_idx");
utils::executeCommand(dboSession, "DROP INDEX IF EXISTS track_recording_mbid_idx");
auto convertMBIDColumn = [&](std::string_view table, std::string_view column) {
LMS_LOG(DB, INFO, "Migrating '" << column << "' from table '" << table << "'...");
utils::executeCommand(dboSession, "ALTER TABLE " + std::string{ table } + " ADD COLUMN " + std::string{ column } + "_new BLOB");
// Fetch all non-empty MBIDs and convert to blobs
using Row = std::tuple<long long, std::string>;
std::vector<std::pair<long long, std::vector<unsigned char>>> rows;
const auto queryResults{ dboSession.query<Row>("SELECT id, " + std::string{ column } + " FROM " + std::string{ table } + " WHERE " + std::string{ column } + " != ''") };
std::string hexStr;
hexStr.reserve(32);
utils::forEachQueryResult(queryResults, [&](const Row& row) {
hexStr.clear();
const auto& uuid{ std::get<1>(row) };
for (char c : uuid)
{
if (c != '-')
hexStr.push_back(c);
}
const auto blob{ core::stringUtils::stringFromHex(hexStr) };
if (blob && blob->size() == 16)
rows.emplace_back(std::get<0>(row), std::vector<unsigned char>{ blob->begin(), blob->end() });
});
// Update with prepared statement loop
Wt::Dbo::Transaction transaction{ dboSession };
auto update{ transaction.connection()->prepareStatement("UPDATE " + std::string{ table } + " SET " + std::string{ column } + "_new = ? WHERE id = ?") };
for (const auto& [id, blob] : rows)
{
update->bind(0, blob);
update->bind(1, id);
update->execute();
update->reset();
}
utils::executeCommand(dboSession, "ALTER TABLE " + std::string{ table } + " DROP COLUMN " + std::string{ column });
utils::executeCommand(dboSession, "ALTER TABLE " + std::string{ table } + " RENAME COLUMN " + std::string{ column } + "_new TO " + std::string{ column });
};
convertMBIDColumn("artist", "mbid");
convertMBIDColumn("release", "mbid");
convertMBIDColumn("release", "group_mbid");
convertMBIDColumn("track", "mbid");
convertMBIDColumn("track", "recording_mbid");
}
void migrateFromV107(Session& session)
{
// Extract Genre, Mood, Language and Grouping from Cluster (keep it for user tags)
LMS_LOG(DB, INFO, "Migrating genre...");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "genre" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"track_count" integer not null default 0,
"release_count" integer not null default 0
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_genre" (
"track_id" bigint,
"genre_id" bigint,
primary key ("track_id", "genre_id"),
constraint "fk_track_genre_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_genre_genre" foreign key ("genre_id") references "genre" ("id") on delete cascade deferrable initially deferred
))");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO genre (version, name, track_count, release_count)
SELECT 0, c.name, SUM(c.track_count), SUM(c.release_count) FROM cluster c
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
WHERE ct.name = 'GENRE'
GROUP BY c.name)");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_genre (track_id, genre_id)
SELECT DISTINCT tc.track_id, g.id FROM track_cluster tc
INNER JOIN cluster c ON c.id = tc.cluster_id
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
INNER JOIN genre g ON g.name = c.name
WHERE ct.name = 'GENRE')");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_genre_genre" ON "track_genre" ("genre_id"))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_genre_track" ON "track_genre" ("track_id"))");
LMS_LOG(DB, INFO, "Migrating mood...");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "mood" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_mood" (
"track_id" bigint,
"mood_id" bigint,
primary key ("track_id", "mood_id"),
constraint "fk_track_mood_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_mood_mood" foreign key ("mood_id") references "mood" ("id") on delete cascade deferrable initially deferred
))");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO mood (version, name)
SELECT DISTINCT 0, c.name FROM cluster c
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
WHERE ct.name = 'MOOD')");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_mood (track_id, mood_id)
SELECT DISTINCT tc.track_id, m.id FROM track_cluster tc
INNER JOIN cluster c ON c.id = tc.cluster_id
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
INNER JOIN mood m ON m.name = c.name
WHERE ct.name = 'MOOD')");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_mood_mood" ON "track_mood" ("mood_id"))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_mood_track" ON "track_mood" ("track_id"))");
LMS_LOG(DB, INFO, "Migrating language...");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "language" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_language" (
"track_id" bigint,
"language_id" bigint,
primary key ("track_id", "language_id"),
constraint "fk_track_language_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_language_language" foreign key ("language_id") references "language" ("id") on delete cascade deferrable initially deferred
))");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO language (version, name)
SELECT DISTINCT 0, c.name FROM cluster c
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
WHERE ct.name = 'LANGUAGE')");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_language (track_id, language_id)
SELECT DISTINCT tc.track_id, l.id FROM track_cluster tc
INNER JOIN cluster c ON c.id = tc.cluster_id
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
INNER JOIN language l ON l.name = c.name
WHERE ct.name = 'LANGUAGE')");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_language_language" ON "track_language" ("language_id"))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_language_track" ON "track_language" ("track_id"))");
LMS_LOG(DB, INFO, "Migrating grouping...");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "grouping" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_grouping" (
"track_id" bigint,
"grouping_id" bigint,
primary key ("track_id", "grouping_id"),
constraint "fk_track_grouping_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_grouping_grouping" foreign key ("grouping_id") references "grouping" ("id") on delete cascade deferrable initially deferred
))");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO grouping (version, name)
SELECT DISTINCT 0, c.name FROM cluster c
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
WHERE ct.name = 'GROUPING')");
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_grouping (track_id, grouping_id)
SELECT DISTINCT tc.track_id, g.id FROM track_cluster tc
INNER JOIN cluster c ON c.id = tc.cluster_id
INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id
INNER JOIN grouping g ON g.name = c.name
WHERE ct.name = 'GROUPING')");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_grouping_grouping" ON "track_grouping" ("grouping_id"))");
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_grouping_track" ON "track_grouping" ("track_id"))");
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM track_cluster WHERE cluster_id IN (SELECT c.id FROM cluster c INNER JOIN cluster_type ct ON ct.id = c.cluster_type_id WHERE ct.name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING')))");
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE cluster DROP COLUMN track_count)");
utils::executeCommand(*session.getDboSession(), R"(ALTER TABLE cluster DROP COLUMN release_count)");
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster WHERE cluster_type_id IN (SELECT id 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'))");
}
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)" };
@@ -1810,14 +2003,16 @@ FROM track)");
{ 102, migrateFromV102 }, { 102, migrateFromV102 },
{ 103, migrateFromV103 }, { 103, migrateFromV103 },
{ 104, migrateFromV104 }, { 104, migrateFromV104 },
{ 105, migrateFromV105 },
{ 106, migrateFromV106 },
{ 107, migrateFromV107 },
}; };
bool migrationPerformed{};
{
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration"); LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
auto transaction{ session.createWriteTransaction() };
Version version; Version version{};
{
auto transaction{ session.createWriteTransaction() };
try try
{ {
version = VersionInfo::getOrCreate(session)->getVersion(); version = VersionInfo::getOrCreate(session)->getVersion();
@@ -1834,7 +2029,9 @@ FROM track)");
if (version < migrationFunctions.begin()->first) if (version < migrationFunctions.begin()->first)
throw core::LmsException{ outdatedMsg }; throw core::LmsException{ outdatedMsg };
}
bool migrationPerformed{};
while (version < LMS_DATABASE_VERSION) while (version < LMS_DATABASE_VERSION)
{ {
LMS_SCOPED_TRACE_DETAILED("Database", "MigrationStep"); LMS_SCOPED_TRACE_DETAILED("Database", "MigrationStep");
@@ -1844,14 +2041,16 @@ FROM track)");
if (itMigrationFunc == std::cend(migrationFunctions)) if (itMigrationFunc == std::cend(migrationFunctions))
throw core::LmsException{ "No code found to upgrade database!" }; throw core::LmsException{ "No code found to upgrade database!" };
{
auto transaction{ session.createWriteTransaction() };
itMigrationFunc->second(session); itMigrationFunc->second(session);
VersionInfo::get(session).modify()->setVersion(version + 1);
VersionInfo::get(session).modify()->setVersion(++version); }
++version;
LMS_LOG(DB, INFO, "Migration complete to version " << version); LMS_LOG(DB, INFO, "Migration complete to version " << version);
migrationPerformed = true; migrationPerformed = true;
} }
}
return migrationPerformed; return migrationPerformed;
} }
+153 -134
View File
@@ -30,10 +30,14 @@
#include "database/objects/AuthToken.hpp" #include "database/objects/AuthToken.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Listen.hpp" #include "database/objects/Listen.hpp"
#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/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"
@@ -66,6 +70,7 @@
#include "traits/ImageHashTypeTraits.hpp" #include "traits/ImageHashTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp" #include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp" #include "traits/PathTraits.hpp"
#include "traits/UUIDTraits.hpp"
namespace lms::db namespace lms::db
{ {
@@ -80,6 +85,10 @@ namespace lms::db
_session.mapClass<AuthToken>("auth_token"); _session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster"); _session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type"); _session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Genre>("genre");
_session.mapClass<Grouping>("grouping");
_session.mapClass<Language>("language");
_session.mapClass<Mood>("mood");
_session.mapClass<Country>("country"); _session.mapClass<Country>("country");
_session.mapClass<Directory>("directory"); _session.mapClass<Directory>("directory");
_session.mapClass<Image>("image"); _session.mapClass<Image>("image");
@@ -185,141 +194,151 @@ namespace lms::db
LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation");
LMS_LOG(DB, INFO, "Creating indexes... This may take a while..."); LMS_LOG(DB, INFO, "Creating indexes... This may take a while...");
constexpr std::string_view indexSqls[]{
"CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)",
"CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)",
"CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)",
"CREATE INDEX IF NOT EXISTS artist_info_path_idx ON artist_info(absolute_file_path)",
"CREATE INDEX IF NOT EXISTS artist_info_directory_id_idx ON artist_info(directory_id)",
"CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)",
"CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)",
"CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)",
"CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)",
"CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)",
"CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)",
"CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)",
"CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)",
"CREATE INDEX IF NOT EXISTS cluster_id_idx ON cluster(id)",
"CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)",
"CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)",
"CREATE UNIQUE INDEX IF NOT EXISTS genre_name_idx ON genre(name)",
"CREATE UNIQUE INDEX IF NOT EXISTS grouping_name_idx ON grouping(name)",
"CREATE UNIQUE INDEX IF NOT EXISTS language_name_idx ON language(name)",
"CREATE UNIQUE INDEX IF NOT EXISTS mood_name_idx ON mood(name)",
"CREATE INDEX IF NOT EXISTS country_id_idx ON country(id)",
"CREATE INDEX IF NOT EXISTS country_name_idx ON country(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)",
"CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)",
"CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)",
"CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)",
"CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)",
"CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_track_embedded_image_id_idx ON track_embedded_image_link(track_id, track_embedded_image_id)",
"CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)",
"CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)",
"CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS label_id_idx ON label(id)",
"CREATE INDEX IF NOT EXISTS label_name_idx ON label(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)",
"CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)",
"CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)",
"CREATE INDEX IF NOT EXISTS listen_user_backend_date_time_idx ON listen(user_id, backend, date_time DESC)",
"CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)",
"CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)",
"CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)",
"CREATE INDEX IF NOT EXISTS medium_release_position_idx ON medium(release_id, position)",
"CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)",
"CREATE INDEX IF NOT EXISTS playlist_file_directory_idx ON playlist_file(directory_id)",
"CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)",
"CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)",
"CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)",
"CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)",
"CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)",
"CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)",
"CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)",
"CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)",
"CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS release_sort_name_idx ON release(sort_name)",
"CREATE INDEX IF NOT EXISTS release_sort_name_nocase_idx ON release(sort_name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS release_artist_link_id_idx ON release_artist_link(id)",
"CREATE INDEX IF NOT EXISTS release_artist_link_artist_idx ON release_artist_link(artist_id)",
"CREATE INDEX IF NOT EXISTS release_artist_link_release_idx ON release_artist_link(release_id)",
"CREATE INDEX IF NOT EXISTS release_type_id_idx ON release_type(id)",
"CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)",
"CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)",
"CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)",
"CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id)",
"CREATE INDEX IF NOT EXISTS track_file_added_idx ON track(file_added)",
"CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)",
"CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)",
"CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)",
"CREATE INDEX IF NOT EXISTS track_medium_idx ON track(medium_id)",
"CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)",
"CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)",
"CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)",
"CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)",
"CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)",
"CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)",
"CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)",
"CREATE INDEX IF NOT EXISTS tracklist_id_idx ON tracklist(id)",
"CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)",
"CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)",
"CREATE INDEX IF NOT EXISTS tracklist_last_modified_date_time_idx ON tracklist(last_modified_date_time)",
"CREATE INDEX IF NOT EXISTS tracklist_entry_idx ON tracklist_entry(id)",
"CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_id_idx ON track_artist_link(id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_artist_mbid_matched_artist_idx ON track_artist_link(artist_mbid_matched, artist_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_track_idx ON track_artist_link(artist_id, type, track_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)",
"CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id, type)",
"CREATE INDEX IF NOT EXISTS track_musicnn_embeddings_track_idx ON track_musicnn_embeddings(track_id)",
"CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)",
"CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)",
"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_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 starred_artist_user_backend_idx ON starred_artist(user_id,backend)",
"CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)",
"CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)",
"CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)",
"CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)",
"CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)",
};
for (std::string_view sql : indexSqls)
{ {
auto transaction{ createWriteTransaction() }; auto transaction{ createWriteTransaction() };
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); utils::executeCommand(_session, std::string{ sql });
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_path_idx ON artist_info(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_directory_id_idx ON artist_info(directory_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_id_idx ON cluster(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_id_idx ON country(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS country_name_idx ON country(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_track_embedded_image_id_idx ON track_embedded_image_link(track_id, track_embedded_image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_id_idx ON label(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS label_name_idx ON label(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_backend_date_time_idx ON listen(user_id, backend, date_time DESC)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS medium_release_position_idx ON medium(release_id, position)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_directory_idx ON playlist_file(directory_id);");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_idx ON release(sort_name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_nocase_idx ON release(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_id_idx ON release_artist_link(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_artist_idx ON release_artist_link(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_release_idx ON release_artist_link(release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_id_idx ON release_type(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_added_idx ON track(file_added)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_medium_idx ON track(medium_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_id_idx ON tracklist(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_last_modified_date_time_idx ON tracklist(last_modified_date_time)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_idx ON tracklist_entry(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_id_idx ON track_artist_link(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_mbid_matched_artist_idx ON track_artist_link(artist_mbid_matched, artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_track_idx ON track_artist_link(artist_id, type, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id, type)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_musicnn_embeddings_track_idx ON track_musicnn_embeddings(track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_id_idx ON track_lyrics(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_absolute_file_path_idx ON track_lyrics(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_directory_idx ON track_lyrics(directory_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
} }
LMS_LOG(DB, INFO, "Indexes created!"); LMS_LOG(DB, INFO, "Indexes created!");
@@ -346,7 +365,7 @@ namespace lms::db
LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum"); LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum");
LMS_LOG(DB, INFO, "Performing vacuum... This may take a while..."); LMS_LOG(DB, INFO, "Performing vacuum... This may take a while...");
// We manually take a lock here since vacuum cannot be inside a transaction // We manually take a lock here since vacuum cannot be inside a transaction.
{ {
std::unique_lock lock{ static_cast<Db&>(_db).getMutex() }; std::unique_lock lock{ static_cast<Db&>(_db).getMutex() };
static_cast<Db&>(_db).executeSql("VACUUM"); static_cast<Db&>(_db).executeSql("VACUUM");
+44 -14
View File
@@ -26,6 +26,10 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -35,6 +39,7 @@
#include "objects/detail/Types.hpp" #include "objects/detail/Types.hpp"
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp" #include "traits/StringViewTraits.hpp"
#include "traits/UUIDTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Artist) DBO_INSTANTIATE_TEMPLATES(lms::db::Artist)
@@ -54,6 +59,10 @@ namespace lms::db
|| params.trackArtistLinkType.has_value() || params.trackArtistLinkType.has_value()
|| params.track.isValid() || params.track.isValid()
|| params.filters.clusters.size() == 1 || params.filters.clusters.size() == 1
|| params.filters.genre.isValid()
|| params.filters.grouping.isValid()
|| params.filters.language.isValid()
|| params.filters.mood.isValid()
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.filters.label.isValid() || params.filters.label.isValid()
|| params.filters.releaseType.isValid()) || params.filters.releaseType.isValid())
@@ -161,6 +170,34 @@ namespace lms::db
query.where(oss.str()); query.where(oss.str());
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t_a_l.track_id")
.where("t_g.genre_id = ?")
.bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t_a_l.track_id")
.where("t_gr.grouping_id = ?")
.bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t_a_l.track_id")
.where("t_l.language_id = ?")
.bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t_a_l.track_id")
.where("t_m.mood_id = ?")
.bind(params.filters.mood);
}
if (params.track.isValid()) if (params.track.isValid())
query.where("t_a_l.track_id = ?").bind(params.track); query.where("t_a_l.track_id = ?").bind(params.track);
@@ -214,7 +251,7 @@ namespace lms::db
} // namespace } // namespace
Artist::Artist(const std::string& name, const std::optional<core::UUID>& mbid) Artist::Artist(const std::string& name, const std::optional<core::UUID>& mbid)
: _mbid{ mbid ? mbid->getAsString() : "" } : _mbid{ mbid }
{ {
setName(name); setName(name);
} }
@@ -240,7 +277,11 @@ namespace lms::db
if (library.isValid()) if (library.isValid())
{ {
// Faster than using joins // Faster than using joins
query.where("EXISTS (SELECT 1 FROM track_artist_link t_a_l JOIN track t ON t.id = t_a_l.track_id WHERE t_a_l.artist_id = a.id AND t.media_library_id = ?)").bind(library); query.where(
"EXISTS (SELECT 1 FROM track_artist_link t_a_l JOIN track t ON t.id = t_a_l.track_id WHERE t_a_l.artist_id = a.id AND t.media_library_id = ?)"
" OR EXISTS (SELECT 1 FROM release_artist_link r_a_l JOIN release r ON r.id = r_a_l.release_id JOIN track t ON t.release_id = r.id WHERE r_a_l.artist_id = a.id AND t.media_library_id = ?)")
.bind(library)
.bind(library);
} }
utils::forEachQueryResult(query, [&](const Artist::pointer& artist) { utils::forEachQueryResult(query, [&](const Artist::pointer& artist) {
@@ -273,7 +314,7 @@ namespace lms::db
Artist::pointer Artist::find(Session& session, const core::UUID& mbid) Artist::pointer Artist::find(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.mbid = ?").bind(std::string{ mbid.getAsString() })); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.mbid = ?").bind(mbid));
} }
Artist::pointer Artist::find(Session& session, ArtistId id) Artist::pointer Artist::find(Session& session, ArtistId id)
@@ -382,17 +423,6 @@ AND NOT EXISTS (
utils::executeCommand(*session.getDboSession(), "UPDATE artist SET preferred_artwork_id = NULL WHERE id = ?", artistId); utils::executeCommand(*session.getDboSession(), "UPDATE artist SET preferred_artwork_id = NULL WHERE id = ?", artistId);
} }
std::optional<core::UUID> Artist::getMBID() const
{
return core::UUID::fromString(_mbid);
}
bool Artist::hasMBID() const
{
// TODO optim this
return getMBID().has_value();
}
ObjectPtr<Artwork> Artist::getPreferredArtwork() const ObjectPtr<Artwork> Artist::getPreferredArtwork() const
{ {
return ObjectPtr<Artwork>{ _preferredArtwork }; return ObjectPtr<Artwork>{ _preferredArtwork };
@@ -112,13 +112,13 @@ namespace lms::db
query.where("a_i.mbid_matched = FALSE"); query.where("a_i.mbid_matched = FALSE");
if (!allowArtistMBIDFallback) if (!allowArtistMBIDFallback)
{ {
query.where("a.mbid <> ''"); query.where("a.mbid IS NOT NULL");
} }
else else
{ {
query.where(R"( query.where(R"(
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid)) (a.mbid IS NOT NULL AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL AND a2.mbid <> a.mbid))
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))"); OR (a.mbid IS NULL AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL) = 1))");
} }
utils::applyRange(query, range); utils::applyRange(query, range);
@@ -20,7 +20,6 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
@@ -92,18 +91,6 @@ namespace lms::db
return res; return res;
} }
Wt::WDateTime Artwork::getLastWrittenTime() const
{
auto query{ session()->query<Wt::WDateTime>("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") };
query.leftJoin("image ON artwork.image_id = image.id");
query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id");
query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id");
query.leftJoin("track ON track.id = track_embedded_image_link.track_id");
query.where("artwork.id = ?").bind(getId());
return utils::fetchQuerySingleResult(query);
}
std::filesystem::path Artwork::getAbsoluteFilePath() const std::filesystem::path Artwork::getAbsoluteFilePath() const
{ {
auto query{ session()->query<std::filesystem::path>("SELECT COALESCE(image.absolute_file_path, track.absolute_file_path) AS absolute_file_path FROM artwork") }; auto query{ session()->query<std::filesystem::path>("SELECT COALESCE(image.absolute_file_path, track.absolute_file_path) AS absolute_file_path FROM artwork") };
+6 -1
View File
@@ -25,8 +25,12 @@
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Directory.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/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.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"
@@ -51,7 +55,6 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM cluster c") }; auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM cluster c") };
query.groupBy("c.id");
if (params.track.isValid() || params.release.isValid()) if (params.track.isValid() || params.release.isValid())
query.join("track_cluster t_c ON t_c.cluster_id = c.id"); query.join("track_cluster t_c ON t_c.cluster_id = c.id");
@@ -83,6 +86,8 @@ namespace lms::db
break; break;
} }
// track_cluster has a UNIQUE constraint on (track_id, cluster_id), so no duplicates can occur when filtering by track
if (!params.track.isValid())
query.groupBy("c.id"); query.groupBy("c.id");
return query; return query;
+187
View File
@@ -0,0 +1,187 @@
/*
* 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/Genre.hpp"
#include <Wt/Dbo/Impl.h>
#include "core/ILogger.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/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 "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Genre)
namespace lms::db
{
namespace
{
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Genre::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM genre g") };
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|| params.sortMethod == GenreSortMethod::TrackCountDesc)
query.join("track_genre t_g ON t_g.genre_id = g.id");
if (params.track.isValid())
query.where("t_g.track_id = ?").bind(params.track);
if (params.release.isValid() || params.artist.isValid())
query.join("track t ON t.id = t_g.track_id");
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
query.where("t_a_l.artist_id = ?").bind(params.artist);
}
switch (params.sortMethod)
{
case GenreSortMethod::None:
break;
case GenreSortMethod::Name:
query.orderBy("g.name COLLATE NOCASE");
break;
case GenreSortMethod::TrackCountDesc:
query.orderBy("COUNT(t_g.track_id) DESC");
break;
}
// track_genre has a UNIQUE constraint on (track_id, genre_id), so no duplicates when filtering by track
if (!params.track.isValid())
query.groupBy("g.id");
return query;
}
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Genre::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, GenreId>)
itemToSelect = "g.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Genre>>)
itemToSelect = "g";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} // namespace
Genre::Genre(std::string_view name)
: _name{ name.substr(0, maxNameLength) }
{
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Genre name too long, truncated to '" << _name << "'");
}
Genre::pointer Genre::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Genre>{ new Genre{ name } });
}
std::size_t Genre::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM genre"));
}
RangeResults<GenreId> Genre::findIds(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<GenreId>(session, params) };
return utils::execRangeQuery<GenreId>(query, params.range);
}
RangeResults<Genre::pointer> Genre::find(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) };
return utils::execRangeQuery<Genre::pointer>(query, params.range);
}
void Genre::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Genre>>(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
Genre::pointer Genre::find(Session& session, GenreId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("id = ?").bind(id));
}
Genre::pointer Genre::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > maxNameLength)
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Genre>().where("name = ?").bind(name));
}
RangeResults<GenreId> Genre::findOrphanIds(Session& session, std::optional<Range> range)
{
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)") };
return utils::execRangeQuery<GenreId>(query, range);
}
std::size_t Genre::computeTrackCount(Session& session, GenreId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN track_genre t_g ON t_g.track_id = t.id").where("t_g.genre_id = ?").bind(id));
}
std::size_t Genre::computeReleaseCount(Session& session, GenreId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(DISTINCT t.release_id) FROM track t INNER JOIN track_genre t_g ON t_g.track_id = t.id").where("t_g.genre_id = ?").bind(id));
}
} // namespace lms::db
+175
View File
@@ -0,0 +1,175 @@
/*
* 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/Grouping.hpp"
#include <Wt/Dbo/Impl.h>
#include "core/ILogger.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/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 "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Grouping)
namespace lms::db
{
namespace
{
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Grouping::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM grouping g") };
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|| params.sortMethod == GroupingSortMethod::TrackCountDesc)
query.join("track_grouping t_gr ON t_gr.grouping_id = g.id");
if (params.track.isValid())
query.where("t_gr.track_id = ?").bind(params.track);
if (params.release.isValid() || params.artist.isValid())
query.join("track t ON t.id = t_gr.track_id");
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
query.where("t_a_l.artist_id = ?").bind(params.artist);
}
switch (params.sortMethod)
{
case GroupingSortMethod::None:
break;
case GroupingSortMethod::Name:
query.orderBy("g.name COLLATE NOCASE");
break;
case GroupingSortMethod::TrackCountDesc:
query.orderBy("COUNT(t_gr.track_id) DESC");
break;
}
// track_grouping has a UNIQUE constraint on (track_id, grouping_id), so no duplicates when filtering by track
if (!params.track.isValid())
query.groupBy("g.id");
return query;
}
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Grouping::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, GroupingId>)
itemToSelect = "g.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Grouping>>)
itemToSelect = "g";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} // namespace
Grouping::Grouping(std::string_view name)
: _name{ name.substr(0, maxNameLength) }
{
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Grouping name too long, truncated to '" << _name << "'");
}
Grouping::pointer Grouping::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Grouping>{ new Grouping{ name } });
}
std::size_t Grouping::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM grouping"));
}
RangeResults<GroupingId> Grouping::findIds(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<GroupingId>(session, params) };
return utils::execRangeQuery<GroupingId>(query, params.range);
}
RangeResults<Grouping::pointer> Grouping::find(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) };
return utils::execRangeQuery<Grouping::pointer>(query, params.range);
}
void Grouping::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Grouping>>(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
Grouping::pointer Grouping::find(Session& session, GroupingId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("id = ?").bind(id));
}
Grouping::pointer Grouping::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > maxNameLength)
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Grouping>().where("name = ?").bind(name));
}
RangeResults<GroupingId> Grouping::findOrphanIds(Session& session, std::optional<Range> range)
{
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)") };
return utils::execRangeQuery<GroupingId>(query, range);
}
} // namespace lms::db
+175
View File
@@ -0,0 +1,175 @@
/*
* 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/Language.hpp"
#include <Wt/Dbo/Impl.h>
#include "core/ILogger.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/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 "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Language)
namespace lms::db
{
namespace
{
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Language::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM language l") };
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|| params.sortMethod == LanguageSortMethod::TrackCountDesc)
query.join("track_language t_l ON t_l.language_id = l.id");
if (params.track.isValid())
query.where("t_l.track_id = ?").bind(params.track);
if (params.release.isValid() || params.artist.isValid())
query.join("track t ON t.id = t_l.track_id");
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
query.where("t_a_l.artist_id = ?").bind(params.artist);
}
switch (params.sortMethod)
{
case LanguageSortMethod::None:
break;
case LanguageSortMethod::Name:
query.orderBy("l.name COLLATE NOCASE");
break;
case LanguageSortMethod::TrackCountDesc:
query.orderBy("COUNT(t_l.track_id) DESC");
break;
}
// track_language has a UNIQUE constraint on (track_id, language_id), so no duplicates when filtering by track
if (!params.track.isValid())
query.groupBy("l.id");
return query;
}
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Language::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, LanguageId>)
itemToSelect = "l.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Language>>)
itemToSelect = "l";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} // namespace
Language::Language(std::string_view name)
: _name{ name.substr(0, maxNameLength) }
{
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Language name too long, truncated to '" << _name << "'");
}
Language::pointer Language::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Language>{ new Language{ name } });
}
std::size_t Language::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM language"));
}
RangeResults<LanguageId> Language::findIds(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<LanguageId>(session, params) };
return utils::execRangeQuery<LanguageId>(query, params.range);
}
RangeResults<Language::pointer> Language::find(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) };
return utils::execRangeQuery<Language::pointer>(query, params.range);
}
void Language::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Language>>(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
Language::pointer Language::find(Session& session, LanguageId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("id = ?").bind(id));
}
Language::pointer Language::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > maxNameLength)
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Language>().where("name = ?").bind(name));
}
RangeResults<LanguageId> Language::findOrphanIds(Session& session, std::optional<Range> range)
{
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)") };
return utils::execRangeQuery<LanguageId>(query, range);
}
} // namespace lms::db
+111 -5
View File
@@ -23,6 +23,10 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -53,7 +57,11 @@ namespace lms::db
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.filters.label.isValid() || params.filters.label.isValid()
|| params.filters.releaseType.isValid() || params.filters.releaseType.isValid()
|| params.trackArtistLinkType.has_value()) || params.trackArtistLinkType.has_value()
|| params.filters.genre.isValid()
|| params.filters.grouping.isValid()
|| params.filters.language.isValid()
|| params.filters.mood.isValid())
{ {
query.join("track t ON t.id = t_a_l.track_id"); query.join("track t ON t.id = t_a_l.track_id");
@@ -74,6 +82,30 @@ namespace lms::db
query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id"); query.join("release_release_type r_r_t ON r_r_t.release_id = t.release_id");
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType); query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t.id");
query.where("t_g.genre_id = ?").bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t.id");
query.where("t_l.language_id = ?").bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t.id");
query.where("t_m.mood_id = ?").bind(params.filters.mood);
}
} }
if (params.releaseArtistsOnly) if (params.releaseArtistsOnly)
@@ -156,6 +188,30 @@ namespace lms::db
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType); query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t.id");
query.where("t_g.genre_id = ?").bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t.id");
query.where("t_l.language_id = ?").bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t.id");
query.where("t_m.mood_id = ?").bind(params.filters.mood);
}
if (!params.filters.clusters.empty()) if (!params.filters.clusters.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
@@ -215,6 +271,30 @@ namespace lms::db
query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType); query.where("r_r_t.release_type_id = ?").bind(params.filters.releaseType);
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t.id");
query.where("t_g.genre_id = ?").bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t.id");
query.where("t_gr.grouping_id = ?").bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t.id");
query.where("t_l.language_id = ?").bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t.id");
query.where("t_m.mood_id = ?").bind(params.filters.mood);
}
if (!params.filters.clusters.empty()) if (!params.filters.clusters.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
@@ -382,18 +462,44 @@ namespace lms::db
.bind(releaseId)); .bind(releaseId));
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ReleaseId releaseId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
// TODO not pending remove? // TODO not pending remove?
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l").join("track t ON l.track_id = t.id").where("t.release_id = ?").bind(releaseId).where("l.user_id = ?").bind(userId).where("l.backend = ?").bind(backend).orderBy("l.date_time DESC").limit(1));
// clang-format off
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("user u ON u.id = l.user_id")
.join("track t ON l.track_id = t.id")
.where("t.release_id = ?").bind(releaseId)
.where("l.user_id = ?").bind(userId)
.where("l.backend = u.scrobbling_backend")
.orderBy("l.date_time DESC")
.limit(1)
};
// clang-format on
return utils::fetchQuerySingleResult(query);
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, TrackId trackId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
// TODO not pending remove? // TODO not pending remove?
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l").where("l.track_id = ?").bind(trackId).where("l.user_id = ?").bind(userId).where("l.backend = ?").bind(backend).orderBy("l.date_time DESC").limit(1));
// clang-format off
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("user u ON u.id = l.user_id")
.where("l.track_id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId)
.where("l.backend = u.scrobbling_backend")
.orderBy("l.date_time DESC")
.limit(1)
};
// clang-format on
return utils::fetchQuerySingleResult(query);
} }
} // namespace lms::db } // namespace lms::db
@@ -22,6 +22,10 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -26,7 +26,11 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.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/MediaLibrary.hpp"
#include "database/objects/Mood.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"
+175
View File
@@ -0,0 +1,175 @@
/*
* 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/Mood.hpp"
#include <Wt/Dbo/Impl.h>
#include "core/ILogger.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/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/StringViewTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Mood)
namespace lms::db
{
namespace
{
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Mood::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM mood m") };
if (params.artist.isValid() || params.track.isValid() || params.release.isValid()
|| params.sortMethod == MoodSortMethod::TrackCountDesc)
query.join("track_mood t_m ON t_m.mood_id = m.id");
if (params.track.isValid())
query.where("t_m.track_id = ?").bind(params.track);
if (params.release.isValid() || params.artist.isValid())
query.join("track t ON t.id = t_m.track_id");
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
query.where("t_a_l.artist_id = ?").bind(params.artist);
}
switch (params.sortMethod)
{
case MoodSortMethod::None:
break;
case MoodSortMethod::Name:
query.orderBy("m.name COLLATE NOCASE");
break;
case MoodSortMethod::TrackCountDesc:
query.orderBy("COUNT(t_m.track_id) DESC");
break;
}
// track_mood has a UNIQUE constraint on (track_id, mood_id), so no duplicates when filtering by track
if (!params.track.isValid())
query.groupBy("m.id");
return query;
}
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Mood::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, MoodId>)
itemToSelect = "m.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Mood>>)
itemToSelect = "m";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} // namespace
Mood::Mood(std::string_view name)
: _name{ name.substr(0, maxNameLength) }
{
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Mood name too long, truncated to '" << _name << "'");
}
Mood::pointer Mood::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Mood>{ new Mood{ name } });
}
std::size_t Mood::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM mood"));
}
RangeResults<MoodId> Mood::findIds(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<MoodId>(session, params) };
return utils::execRangeQuery<MoodId>(query, params.range);
}
RangeResults<Mood::pointer> Mood::find(Session& session, const FindParameters& params)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) };
return utils::execRangeQuery<Mood::pointer>(query, params.range);
}
void Mood::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Mood>>(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
Mood::pointer Mood::find(Session& session, MoodId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("id = ?").bind(id));
}
Mood::pointer Mood::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > maxNameLength)
name = name.substr(0, maxNameLength);
return utils::fetchQuerySingleResult(session.getDboSession()->find<Mood>().where("name = ?").bind(name));
}
RangeResults<MoodId> Mood::findOrphanIds(Session& session, std::optional<Range> range)
{
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)") };
return utils::execRangeQuery<MoodId>(query, range);
}
} // namespace lms::db
@@ -26,7 +26,11 @@
#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"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -27,8 +27,12 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.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/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.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"
@@ -23,6 +23,10 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
+40 -3
View File
@@ -29,8 +29,12 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.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/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.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"
@@ -46,6 +50,7 @@
#include "traits/IdTypeTraits.hpp" #include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp" #include "traits/PartialDateTimeTraits.hpp"
#include "traits/StringViewTraits.hpp" #include "traits/StringViewTraits.hpp"
#include "traits/UUIDTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Country) DBO_INSTANTIATE_TEMPLATES(lms::db::Country)
DBO_INSTANTIATE_TEMPLATES(lms::db::Label) DBO_INSTANTIATE_TEMPLATES(lms::db::Label)
@@ -76,7 +81,11 @@ namespace lms::db
|| params.originalDateRange || params.originalDateRange
|| params.trackArtist.isValid() || params.trackArtist.isValid()
|| params.filters.clusters.size() == 1 || params.filters.clusters.size() == 1
|| params.filters.genre.isValid()
|| params.filters.grouping.isValid()
|| params.filters.language.isValid()
|| params.filters.mediaLibrary.isValid() || params.filters.mediaLibrary.isValid()
|| params.filters.mood.isValid()
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.directory.isValid() || params.directory.isValid()
|| params.parentDirectory.isValid()) || params.parentDirectory.isValid())
@@ -214,11 +223,39 @@ namespace lms::db
query.where(oss.str()); query.where(oss.str());
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t.id")
.where("t_g.genre_id = ?")
.bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t.id")
.where("t_gr.grouping_id = ?")
.bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t.id")
.where("t_l.language_id = ?")
.bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t.id")
.where("t_m.mood_id = ?")
.bind(params.filters.mood);
}
if (params.filters.codec.has_value()) if (params.filters.codec.has_value())
query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value())); query.where("t.codec = ?").bind(detail::getDbCodec(params.filters.codec.value()));
if (params.releaseGroupMBID) if (params.releaseGroupMBID)
query.where("group_mbid = ?").bind(params.releaseGroupMBID->getAsString()); query.where("group_mbid = ?").bind(*params.releaseGroupMBID);
switch (params.sortMethod) switch (params.sortMethod)
{ {
@@ -454,7 +491,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{ std::string(name, 0, _maxNameLength) }
, _MBID{ MBID ? MBID->getAsString() : "" } , _MBID{ MBID }
{ {
} }
@@ -467,7 +504,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT r from release r").where("r.mbid = ?").bind(mbid.getAsString())); return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT r from release r").where("r.mbid = ?").bind(mbid));
} }
Release::pointer Release::find(Session& session, ReleaseId id) Release::pointer Release::find(Session& session, ReleaseId id)
@@ -24,6 +24,10 @@
#include "core/ILogger.hpp" #include "core/ILogger.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/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
@@ -123,13 +127,13 @@ namespace lms::db
query.where("r_a_l.artist_mbid_matched = FALSE"); query.where("r_a_l.artist_mbid_matched = FALSE");
if (!allowArtistMBIDFallback) if (!allowArtistMBIDFallback)
{ {
query.where("a.mbid <> ''"); query.where("a.mbid IS NOT NULL");
} }
else else
{ {
query.where(R"( query.where(R"(
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid)) (a.mbid IS NOT NULL AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL AND a2.mbid <> a.mbid))
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))"); OR (a.mbid IS NULL AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL) = 1))");
} }
utils::applyRange(query, range); utils::applyRange(query, range);
@@ -132,9 +132,9 @@ namespace lms::db
_allowMBIDArtistMerge = value; _allowMBIDArtistMerge = value;
} }
void ScanSettings::setArtistImageFallbackToReleaseField(bool value) void ScanSettings::setArtistImageFallbackToRelease(bool value)
{ {
_artistImageFallbackToReleaseField = value; _artistImageFallbackToRelease = value;
} }
void ScanSettings::incAudioScanVersion() void ScanSettings::incAudioScanVersion()
@@ -23,6 +23,10 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
+130 -13
View File
@@ -30,8 +30,12 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.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/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Mood.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"
@@ -46,6 +50,7 @@
#include "traits/PartialDateTimeTraits.hpp" #include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp" #include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp" #include "traits/StringViewTraits.hpp"
#include "traits/UUIDTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Track) DBO_INSTANTIATE_TEMPLATES(lms::db::Track)
@@ -108,6 +113,34 @@ namespace lms::db
query.where(oss.str()); query.where(oss.str());
} }
if (params.filters.genre.isValid())
{
query.join("track_genre t_g ON t_g.track_id = t.id")
.where("t_g.genre_id = ?")
.bind(params.filters.genre);
}
if (params.filters.grouping.isValid())
{
query.join("track_grouping t_gr ON t_gr.track_id = t.id")
.where("t_gr.grouping_id = ?")
.bind(params.filters.grouping);
}
if (params.filters.language.isValid())
{
query.join("track_language t_l ON t_l.track_id = t.id")
.where("t_l.language_id = ?")
.bind(params.filters.language);
}
if (params.filters.mood.isValid())
{
query.join("track_mood t_m ON t_m.track_id = t.id")
.where("t_m.mood_id = ?")
.bind(params.filters.mood);
}
if (params.artist.isValid() || !params.artistName.empty()) if (params.artist.isValid() || !params.artistName.empty())
{ {
query.join("artist a ON a.id = t_a_l.artist_id") query.join("artist a ON a.id = t_a_l.artist_id")
@@ -392,21 +425,21 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.mbid = ?").bind(mbid.getAsString())); return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.mbid = ?").bind(mbid));
} }
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid) std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Track>>("SELECT t from track t").where("t.recording_mbid = ?").bind(mbid.getAsString())); 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) RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.mbid") }; auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid IS NOT NULL GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.mbid") };
return utils::execRangeQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
@@ -445,6 +478,62 @@ namespace lms::db
return utils::fetchQueryResults(query); return utils::fetchQueryResults(query);
} }
std::vector<Genre::pointer> Track::getGenres() const
{
return utils::fetchQueryResults<Genre::pointer>(_genres.find());
}
std::vector<GenreId> Track::getGenreIds() const
{
assert(session());
const auto query{ session()->query<GenreId>("SELECT t_g.genre_id FROM track_genre t_g").where("t_g.track_id = ?").bind(getId()) };
return utils::fetchQueryResults(query);
}
std::vector<Grouping::pointer> Track::getGroupings() const
{
return utils::fetchQueryResults<Grouping::pointer>(_groupings.find());
}
std::vector<GroupingId> Track::getGroupingIds() const
{
assert(session());
const auto query{ session()->query<GroupingId>("SELECT t_gr.grouping_id FROM track_grouping t_gr").where("t_gr.track_id = ?").bind(getId()) };
return utils::fetchQueryResults(query);
}
std::vector<Language::pointer> Track::getLanguages() const
{
return utils::fetchQueryResults<Language::pointer>(_languages.find());
}
std::vector<LanguageId> Track::getLanguageIds() const
{
assert(session());
const auto query{ session()->query<LanguageId>("SELECT t_l.language_id FROM track_language t_l").where("t_l.track_id = ?").bind(getId()) };
return utils::fetchQueryResults(query);
}
std::vector<Mood::pointer> Track::getMoods() const
{
return utils::fetchQueryResults<Mood::pointer>(_moods.find());
}
std::vector<MoodId> Track::getMoodIds() const
{
assert(session());
const auto query{ session()->query<MoodId>("SELECT t_m.mood_id FROM track_mood t_m").where("t_m.track_id = ?").bind(getId()) };
return utils::fetchQueryResults(query);
}
ObjectPtr<MediaLibrary> Track::getMediaLibrary() const ObjectPtr<MediaLibrary> Track::getMediaLibrary() const
{ {
return _mediaLibrary; return _mediaLibrary;
@@ -565,6 +654,34 @@ namespace lms::db
_clusters.insert(getDboPtr(cluster)); _clusters.insert(getDboPtr(cluster));
} }
void Track::setGenres(std::span<const ObjectPtr<Genre>> genres)
{
_genres.clear();
for (const ObjectPtr<Genre>& genre : genres)
_genres.insert(getDboPtr(genre));
}
void Track::setGroupings(std::span<const ObjectPtr<Grouping>> groupings)
{
_groupings.clear();
for (const ObjectPtr<Grouping>& grouping : groupings)
_groupings.insert(getDboPtr(grouping));
}
void Track::setLanguages(std::span<const ObjectPtr<Language>> languages)
{
_languages.clear();
for (const ObjectPtr<Language>& language : languages)
_languages.insert(getDboPtr(language));
}
void Track::setMoods(std::span<const ObjectPtr<Mood>> moods)
{
_moods.clear();
for (const ObjectPtr<Mood>& mood : moods)
_moods.insert(getDboPtr(mood));
}
void Track::clearLyrics() void Track::clearLyrics()
{ {
_trackLyrics.clear(); _trackLyrics.clear();
@@ -651,8 +768,7 @@ namespace lms::db
std::ostringstream oss; std::ostringstream oss;
oss << "SELECT a from artist a" oss << "SELECT a from artist a"
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id" " INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id";
" INNER JOIN track t ON t.id = t_a_l.track_id";
if (!linkTypes.empty()) if (!linkTypes.empty())
{ {
@@ -663,6 +779,7 @@ namespace lms::db
{ {
if (!first) if (!first)
oss << ", "; oss << ", ";
oss << "?"; oss << "?";
first = false; first = false;
} }
@@ -673,7 +790,7 @@ namespace lms::db
for (TrackArtistLinkType type : linkTypes) for (TrackArtistLinkType type : linkTypes)
query.bind(type); query.bind(type);
query.where("t.id = ?").bind(getId()); query.where("t_a_l.track_id = ?").bind(getId());
query.groupBy("t_a_l.artist_id"); query.groupBy("t_a_l.artist_id");
query.orderBy("t_a_l.id"); query.orderBy("t_a_l.id");
@@ -685,30 +802,30 @@ namespace lms::db
assert(self()); assert(self());
assert(session()); assert(session());
std::ostringstream oss; auto query{ session()->query<ArtistId>("SELECT t_a_l.artist_id FROM track_artist_link t_a_l") };
oss << "SELECT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track t ON t.id = t_a_l.track_id";
if (!linkTypes.empty()) if (!linkTypes.empty())
{ {
oss << " AND t_a_l.type IN ("; std::ostringstream oss;
oss << "t_a_l.type IN (";
bool first{ true }; bool first{ true };
for ([[maybe_unused]] TrackArtistLinkType type : linkTypes) for ([[maybe_unused]] TrackArtistLinkType type : linkTypes)
{ {
if (!first) if (!first)
oss << ", "; oss << ", ";
oss << "?"; oss << "?";
first = false; first = false;
} }
oss << ")"; oss << ")";
}
auto query{ session()->query<ArtistId>(oss.str()) }; query.where(oss.str());
for (TrackArtistLinkType type : linkTypes) for (TrackArtistLinkType type : linkTypes)
query.bind(type); query.bind(type);
}
query.where("t.id = ?").bind(getId()); query.where("t_a_l.track_id = ?").bind(getId());
query.groupBy("t_a_l.artist_id"); query.groupBy("t_a_l.artist_id");
query.orderBy("t_a_l.id"); query.orderBy("t_a_l.id");
@@ -24,6 +24,10 @@
#include "core/ILogger.hpp" #include "core/ILogger.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/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -165,13 +169,13 @@ namespace lms::db
query.where("t_a_l.artist_mbid_matched = FALSE"); query.where("t_a_l.artist_mbid_matched = FALSE");
if (!allowArtistMBIDFallback) if (!allowArtistMBIDFallback)
{ {
query.where("a.mbid <> ''"); query.where("a.mbid IS NOT NULL");
} }
else else
{ {
query.where(R"( query.where(R"(
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid)) (a.mbid IS NOT NULL AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL AND a2.mbid <> a.mbid))
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))"); OR (a.mbid IS NULL AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid IS NOT NULL) = 1))");
} }
utils::applyRange(query, range); utils::applyRange(query, range);
@@ -22,6 +22,10 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -23,6 +23,10 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
@@ -23,6 +23,10 @@
#include <Wt/Dbo/WtSqlTraits.h> #include <Wt/Dbo/WtSqlTraits.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -25,6 +25,10 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.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"
@@ -25,6 +25,10 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
@@ -22,6 +22,10 @@
#include <Wt/Dbo/Impl.h> #include <Wt/Dbo/Impl.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "Utils.hpp" #include "Utils.hpp"
+4
View File
@@ -27,6 +27,10 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/AuthToken.hpp" #include "database/objects/AuthToken.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/UIState.hpp" #include "database/objects/UIState.hpp"
@@ -0,0 +1,64 @@
/*
* 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 <cstddef>
#include <cstring>
#include <span>
#include <vector>
#include <Wt/Dbo/StdSqlTraits.h>
#include "core/UUID.hpp"
namespace Wt::Dbo
{
template<>
struct sql_value_traits<lms::core::UUID>
{
static constexpr bool specialized{ true };
using UnderlyingType = std::vector<unsigned char>;
static std::string type(SqlConnection* conn, int size)
{
return sql_value_traits<UnderlyingType, void>::type(conn, size);
}
static void bind(const lms::core::UUID& v, SqlStatement* statement, int column, int size)
{
constexpr auto binarySize{ lms::core::UUID::binarySize };
const auto bytes{ v.bytes() };
UnderlyingType blob(binarySize);
std::memcpy(blob.data(), bytes.data(), binarySize);
sql_value_traits<UnderlyingType>::bind(blob, statement, column, size);
}
static bool read(lms::core::UUID& v, SqlStatement* statement, int column, int size)
{
constexpr auto binarySize{ lms::core::UUID::binarySize };
UnderlyingType buf;
if (!sql_value_traits<UnderlyingType>::read(buf, statement, column, size) || buf.size() != binarySize)
return false;
v = lms::core::UUID::fromBytes(std::span<const std::byte, binarySize>{ reinterpret_cast<const std::byte*>(buf.data()), binarySize });
return true;
}
};
} // namespace Wt::Dbo
@@ -141,8 +141,8 @@ namespace lms::db
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; } const std::string& getSortName() const { return _sortName; }
std::optional<core::UUID> getMBID() const; std::optional<core::UUID> getMBID() const { return _mbid; }
bool hasMBID() const; bool hasMBID() const { return _mbid.has_value(); }
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const; ArtworkId getPreferredArtworkId() const;
@@ -152,7 +152,7 @@ namespace lms::db
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::span<const ClusterTypeId> clusterTypeIds, std::size_t size) const;
void setName(std::string_view name); void setName(std::string_view name);
void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid; }
void setSortName(std::string_view sortName); void setSortName(std::string_view sortName);
void setPreferredArtwork(ObjectPtr<Artwork> artwork); void setPreferredArtwork(ObjectPtr<Artwork> artwork);
@@ -174,7 +174,7 @@ namespace lms::db
std::string _name; std::string _name;
std::string _sortName; std::string _sortName;
std::string _mbid; // Musicbrainz Identifier std::optional<core::UUID> _mbid;
Wt::Dbo::ptr<Artwork> _preferredArtwork; Wt::Dbo::ptr<Artwork> _preferredArtwork;
}; };
@@ -23,7 +23,6 @@
#include <variant> #include <variant>
#include <Wt/Dbo/Field.h> #include <Wt/Dbo/Field.h>
#include <Wt/WDateTime.h>
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/objects/ArtworkId.hpp" #include "database/objects/ArtworkId.hpp"
@@ -50,7 +49,6 @@ namespace lms::db
// getters // getters
using UnderlyingId = std::variant<std::monostate, TrackEmbeddedImageId, ImageId>; using UnderlyingId = std::variant<std::monostate, TrackEmbeddedImageId, ImageId>;
UnderlyingId getUnderlyingId() const; UnderlyingId getUnderlyingId() const;
Wt::WDateTime getLastWrittenTime() const;
std::filesystem::path getAbsoluteFilePath() const; std::filesystem::path getAbsoluteFilePath() const;
ObjectPtr<Image> getImage() const; ObjectPtr<Image> getImage() const;
ImageId getImageId() const; ImageId getImageId() const;
@@ -68,7 +66,6 @@ namespace lms::db
Artwork(ObjectPtr<Image> image); Artwork(ObjectPtr<Image> image);
static pointer create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage); static pointer create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage);
static pointer create(Session& session, ObjectPtr<Image> image); static pointer create(Session& session, ObjectPtr<Image> image);
Wt::Dbo::ptr<TrackEmbeddedImage> _trackEmbeddedImage; Wt::Dbo::ptr<TrackEmbeddedImage> _trackEmbeddedImage;
Wt::Dbo::ptr<Image> _image; Wt::Dbo::ptr<Image> _image;
}; };
@@ -104,21 +104,13 @@ 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; }
std::size_t getTrackCount() const { return _trackCount; }
RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const; RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
std::size_t getReleasesCount() const { return _releaseCount; };
void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; }
void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; }
void addTrack(ObjectPtr<Track> track); void addTrack(ObjectPtr<Track> track);
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
// cached field since queries are too long
Wt::Dbo::field(a, _trackCount, "track_count");
Wt::Dbo::field(a, _releaseCount, "release_count");
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
@@ -130,8 +122,6 @@ namespace lms::db
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name); static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
std::string _name; std::string _name;
int _trackCount{};
int _releaseCount{};
Wt::Dbo::ptr<ClusterType> _clusterType; Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
@@ -26,8 +26,12 @@
#include "core/media/Codec.hpp" #include "core/media/Codec.hpp"
#include "database/objects/ClusterId.hpp" #include "database/objects/ClusterId.hpp"
#include "database/objects/GenreId.hpp"
#include "database/objects/GroupingId.hpp"
#include "database/objects/LabelId.hpp" #include "database/objects/LabelId.hpp"
#include "database/objects/LanguageId.hpp"
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MoodId.hpp"
#include "database/objects/ReleaseTypeId.hpp" #include "database/objects/ReleaseTypeId.hpp"
namespace lms::db namespace lms::db
@@ -36,7 +40,11 @@ namespace lms::db
{ {
MediaLibraryId mediaLibrary; // tracks that belongs to this library MediaLibraryId mediaLibrary; // tracks that belongs to this library
std::vector<ClusterId> clusters; // tracks that belong to *all* these clusters std::vector<ClusterId> clusters; // tracks that belong to *all* these clusters
GenreId genre; // tracks that belong to this genre
GroupingId grouping; // tracks that belong to this grouping
LabelId label; // tracks which release has this label LabelId label; // tracks which release has this label
LanguageId language; // tracks that belong to this language
MoodId mood; // tracks that belong to this mood
ReleaseTypeId releaseType; // tracks which release has this type ReleaseTypeId releaseType; // tracks which release has this type
std::optional<core::media::Codec> codec; // tracks that match this codec std::optional<core::media::Codec> codec; // tracks that match this codec
@@ -45,6 +53,26 @@ namespace lms::db
clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
return *this; return *this;
} }
Filters& setGenre(GenreId _genre)
{
genre = _genre;
return *this;
}
Filters& setGrouping(GroupingId _grouping)
{
grouping = _grouping;
return *this;
}
Filters& setLanguage(LanguageId _language)
{
language = _language;
return *this;
}
Filters& setMood(MoodId _mood)
{
mood = _mood;
return *this;
}
Filters& setMediaLibrary(MediaLibraryId _mediaLibrary) Filters& setMediaLibrary(MediaLibraryId _mediaLibrary)
{ {
mediaLibrary = _mediaLibrary; mediaLibrary = _mediaLibrary;
@@ -0,0 +1,123 @@
/*
* 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 <Wt/Dbo/collection.h>
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/GenreId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
class Session;
class Track;
class Genre final : public Object<Genre, GenreId>
{
public:
static constexpr std::size_t maxNameLength{ 512 };
struct FindParameters
{
std::optional<Range> range;
GenreSortMethod sortMethod{ GenreSortMethod::None };
ArtistId artist; // if set, genres of tracks by this artist
TrackId track; // if set, genres of this track
ReleaseId release; // if set, genres involved in this release
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setSortMethod(GenreSortMethod _method)
{
sortMethod = _method;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setArtist(ArtistId _artist)
{
artist = _artist;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
};
Genre() = default;
static std::size_t getCount(Session& session);
static RangeResults<GenreId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params);
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, std::string_view name);
static RangeResults<GenreId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static std::size_t computeTrackCount(Session& session, GenreId id);
static std::size_t computeReleaseCount(Session& session, GenreId id);
std::string_view getName() const { return _name; }
std::size_t getTrackCount() const { return _trackCount; }
std::size_t getReleaseCount() const { return _releaseCount; }
void setTrackCount(std::size_t count) { _trackCount = count; }
void setReleaseCount(std::size_t count) { _releaseCount = count; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _trackCount, "track_count");
Wt::Dbo::field(a, _releaseCount, "release_count");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Genre(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
int _trackCount{};
int _releaseCount{};
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(GenreId)
@@ -0,0 +1,110 @@
/*
* 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 <Wt/Dbo/collection.h>
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/GroupingId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
class Session;
class Track;
class Grouping final : public Object<Grouping, GroupingId>
{
public:
static constexpr std::size_t maxNameLength{ 512 };
struct FindParameters
{
std::optional<Range> range;
GroupingSortMethod sortMethod{ GroupingSortMethod::None };
ArtistId artist; // if set, groupings of tracks by this artist
TrackId track; // if set, groupings of this track
ReleaseId release; // if set, groupings involved in this release
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setSortMethod(GroupingSortMethod _method)
{
sortMethod = _method;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setArtist(ArtistId _artist)
{
artist = _artist;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
};
Grouping() = default;
static std::size_t getCount(Session& session);
static RangeResults<GroupingId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params);
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, std::string_view name);
static RangeResults<GroupingId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Grouping(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
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(GroupingId)
@@ -0,0 +1,110 @@
/*
* 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 <Wt/Dbo/collection.h>
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/LanguageId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
class Session;
class Track;
class Language final : public Object<Language, LanguageId>
{
public:
static constexpr std::size_t maxNameLength{ 512 };
struct FindParameters
{
std::optional<Range> range;
LanguageSortMethod sortMethod{ LanguageSortMethod::None };
ArtistId artist; // if set, languages of tracks by this artist
TrackId track; // if set, languages of this track
ReleaseId release; // if set, languages involved in this release
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setSortMethod(LanguageSortMethod _method)
{
sortMethod = _method;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setArtist(ArtistId _artist)
{
artist = _artist;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
};
Language() = default;
static std::size_t getCount(Session& session);
static RangeResults<LanguageId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params);
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, std::string_view name);
static RangeResults<LanguageId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Language(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
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(LanguageId)
@@ -150,8 +150,8 @@ namespace lms::db
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
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId); static pointer getMostRecentListen(Session& session, UserId userId, ReleaseId releaseId); // uses current scrobbling backend
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId); static pointer getMostRecentListen(Session& session, UserId userId, TrackId trackId); // uses current scrobbling backend
SyncState getSyncState() const { return _syncState; } SyncState getSyncState() const { return _syncState; }
ObjectPtr<User> getUser() const { return _user; } ObjectPtr<User> getUser() const { return _user; }
@@ -0,0 +1,110 @@
/*
* 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 <Wt/Dbo/collection.h>
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/MoodId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
class Session;
class Track;
class Mood final : public Object<Mood, MoodId>
{
public:
static constexpr std::size_t maxNameLength{ 512 };
struct FindParameters
{
std::optional<Range> range;
MoodSortMethod sortMethod{ MoodSortMethod::None };
ArtistId artist; // if set, moods of tracks by this artist
TrackId track; // if set, moods of this track
ReleaseId release; // if set, moods involved in this release
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setSortMethod(MoodSortMethod _method)
{
sortMethod = _method;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setArtist(ArtistId _artist)
{
artist = _artist;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
};
Mood() = default;
static std::size_t getCount(Session& session);
static RangeResults<MoodId> findIds(Session& session, const FindParameters& params);
static RangeResults<pointer> find(Session& session, const FindParameters& params);
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, std::string_view name);
static RangeResults<MoodId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
Mood(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
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(MoodId)
@@ -298,8 +298,8 @@ namespace lms::db
// Accessors // Accessors
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
std::string_view getSortName() const { return _sortName; } std::string_view getSortName() const { return _sortName; }
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); } std::optional<core::UUID> getMBID() const { return _MBID; }
std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); } std::optional<core::UUID> getGroupMBID() const { return _groupMBID; }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } // the number of discs this release should have if complete std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } // the number of discs this release should have if complete
std::chrono::milliseconds getDuration() const; std::chrono::milliseconds getDuration() const;
Wt::WDateTime getAddedTime() const; Wt::WDateTime getAddedTime() const;
@@ -325,8 +325,8 @@ namespace lms::db
// Setters // Setters
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
void setSortName(std::string_view sortName) { _sortName = sortName; } void setSortName(std::string_view sortName) { _sortName = sortName; }
void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid; }
void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; } void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid; }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; } void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void clearArtistLinks(); void clearArtistLinks();
@@ -382,8 +382,8 @@ namespace lms::db
std::string _name; std::string _name;
std::string _sortName; std::string _sortName;
std::string _MBID; std::optional<core::UUID> _MBID;
std::string _groupMBID; std::optional<core::UUID> _groupMBID;
std::optional<int> _totalDisc{}; std::optional<int> _totalDisc{};
std::string _artistDisplayName; std::string _artistDisplayName;
bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5 bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5
@@ -75,7 +75,7 @@ namespace lms::db
std::vector<std::string> getArtistsToNotSplit() const; std::vector<std::string> getArtistsToNotSplit() const;
bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; } bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; }
bool getAllowMBIDArtistMerge() const { return _allowMBIDArtistMerge; } bool getAllowMBIDArtistMerge() const { return _allowMBIDArtistMerge; }
bool getArtistImageFallbackToReleaseField() const { return _artistImageFallbackToReleaseField; } bool getArtistImageFallbackToRelease() const { return _artistImageFallbackToRelease; }
// Setters // Setters
void setUpdateStartTime(Wt::WTime t) { _startTime = t; } void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
@@ -87,7 +87,7 @@ namespace lms::db
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters); void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
void setSkipSingleReleasePlayLists(bool value); void setSkipSingleReleasePlayLists(bool value);
void setAllowMBIDArtistMerge(bool value); void setAllowMBIDArtistMerge(bool value);
void setArtistImageFallbackToReleaseField(bool value); void setArtistImageFallbackToRelease(bool value);
void setMusicNNModelIdentifier(std::string_view identifier) { _musicnnModelIdentifier = identifier; } void setMusicNNModelIdentifier(std::string_view identifier) { _musicnnModelIdentifier = identifier; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -104,7 +104,7 @@ namespace lms::db
Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters"); Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters");
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists"); Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge"); Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
Wt::Dbo::field(a, _artistImageFallbackToReleaseField, "artist_image_fallback_to_release"); Wt::Dbo::field(a, _artistImageFallbackToRelease, "artist_image_fallback_to_release");
Wt::Dbo::field(a, _musicnnModelIdentifier, "musicnn_model_identifier"); Wt::Dbo::field(a, _musicnnModelIdentifier, "musicnn_model_identifier");
} }
@@ -126,9 +126,9 @@ namespace lms::db
std::string _artistTagDelimiters; std::string _artistTagDelimiters;
std::string _artistsToNotSplit; std::string _artistsToNotSplit;
std::string _defaultTagDelimiters; std::string _defaultTagDelimiters;
bool _skipSingleReleasePlayLists{}; bool _skipSingleReleasePlayLists{ true };
bool _allowMBIDArtistMerge{}; bool _allowMBIDArtistMerge{ true };
bool _artistImageFallbackToReleaseField{}; bool _artistImageFallbackToRelease{ true };
std::string _musicnnModelIdentifier; std::string _musicnnModelIdentifier;
}; };
} // namespace lms::db } // namespace lms::db
@@ -45,8 +45,12 @@
#include "database/objects/ClusterId.hpp" #include "database/objects/ClusterId.hpp"
#include "database/objects/DirectoryId.hpp" #include "database/objects/DirectoryId.hpp"
#include "database/objects/Filters.hpp" #include "database/objects/Filters.hpp"
#include "database/objects/GenreId.hpp"
#include "database/objects/GroupingId.hpp"
#include "database/objects/LanguageId.hpp"
#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/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"
@@ -62,6 +66,10 @@ namespace lms::db
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Directory; class Directory;
class Genre;
class Grouping;
class Language;
class Mood;
class TrackEmbeddedImageLink; class TrackEmbeddedImageLink;
class MediaLibrary; class MediaLibrary;
class Medium; class Medium;
@@ -258,8 +266,8 @@ namespace lms::db
void setName(std::string_view name); void setName(std::string_view name);
void setDate(const core::PartialDateTime& date) { _date = date; } void setDate(const core::PartialDateTime& date) { _date = date; }
void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; } void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; }
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID; }
void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID; }
void setCopyright(std::string_view copyright); void setCopyright(std::string_view copyright);
void setCopyrightURL(std::string_view copyrightURL); void setCopyrightURL(std::string_view copyrightURL);
void setAdvisory(Advisory advisory) { _advisory = advisory; } void setAdvisory(Advisory advisory) { _advisory = advisory; }
@@ -270,6 +278,10 @@ namespace lms::db
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); } void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
void setMedium(ObjectPtr<Medium> medium) { _medium = getDboPtr(medium); } void setMedium(ObjectPtr<Medium> medium) { _medium = getDboPtr(medium); }
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters); void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
void setGenres(std::span<const ObjectPtr<Genre>> genres);
void setGroupings(std::span<const ObjectPtr<Grouping>> groupings);
void setLanguages(std::span<const ObjectPtr<Language>> languages);
void setMoods(std::span<const ObjectPtr<Mood>> moods);
void clearLyrics(); void clearLyrics();
void clearEmbeddedLyrics(); void clearEmbeddedLyrics();
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics); void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
@@ -308,8 +320,8 @@ namespace lms::db
std::optional<int> getOriginalYear() const; std::optional<int> getOriginalYear() const;
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; } const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
bool hasLyrics() const; bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); } std::optional<core::UUID> getTrackMBID() const { return _trackMBID; }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } std::optional<core::UUID> getRecordingMBID() const { return _recordingMBID; }
std::string_view getCopyright() const; std::string_view getCopyright() const;
std::string_view getCopyrightURL() const; std::string_view getCopyrightURL() const;
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::string_view getArtistDisplayName() const { return _artistDisplayName; }
@@ -330,6 +342,14 @@ namespace lms::db
ObjectPtr<Medium> getMedium() const { return _medium; } ObjectPtr<Medium> getMedium() const { return _medium; }
std::vector<ObjectPtr<Cluster>> getClusters() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> getClusterIds() const; std::vector<ClusterId> getClusterIds() const;
std::vector<ObjectPtr<Genre>> getGenres() const;
std::vector<GenreId> getGenreIds() const;
std::vector<ObjectPtr<Grouping>> getGroupings() const;
std::vector<GroupingId> getGroupingIds() const;
std::vector<ObjectPtr<Language>> getLanguages() const;
std::vector<LanguageId> getLanguageIds() const;
std::vector<ObjectPtr<Mood>> getMoods() const;
std::vector<MoodId> getMoodIds() 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;
@@ -378,6 +398,10 @@ namespace lms::db
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, _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, _groupings, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _languages, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _moods, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _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");
} }
@@ -413,8 +437,8 @@ namespace lms::db
std::string _name; std::string _name;
core::PartialDateTime _date; core::PartialDateTime _date;
core::PartialDateTime _originalDate; core::PartialDateTime _originalDate;
std::string _trackMBID; std::optional<core::UUID> _trackMBID;
std::string _recordingMBID; std::optional<core::UUID> _recordingMBID;
std::string _copyright; std::string _copyright;
std::string _copyrightURL; std::string _copyrightURL;
std::string _artistDisplayName; std::string _artistDisplayName;
@@ -429,6 +453,10 @@ namespace lms::db
Wt::Dbo::ptr<Artwork> _preferredMediaArtwork; Wt::Dbo::ptr<Artwork> _preferredMediaArtwork;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters; Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<Genre>> _genres;
Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings;
Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages;
Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods;
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;
}; };
@@ -65,6 +65,20 @@ namespace lms::db
Name, Name,
}; };
enum class GenreSortMethod
{
None,
Name,
TrackCountDesc,
};
enum class GroupingSortMethod
{
None,
Name,
TrackCountDesc,
};
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>; using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
enum class LabelSortMethod enum class LabelSortMethod
@@ -73,6 +87,20 @@ namespace lms::db
Name, Name,
}; };
enum class LanguageSortMethod
{
None,
Name,
TrackCountDesc,
};
enum class MoodSortMethod
{
None,
Name,
TrackCountDesc,
};
enum class MediumSortMethod enum class MediumSortMethod
{ {
None, None,
@@ -198,6 +226,7 @@ namespace lms::db
{ {
Internal = 0, Internal = 0,
ListenBrainz = 1, ListenBrainz = 1,
LastFm = 2,
}; };
enum class FeedbackBackend enum class FeedbackBackend
@@ -118,6 +118,9 @@ namespace lms::db
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; } void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; } void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
void setListenBrainzToken(std::string_view token) { _listenbrainzToken = token; } void setListenBrainzToken(std::string_view token) { _listenbrainzToken = token; }
void setLastFmApiKey(std::string_view key) { _lastFmApiKey = key; }
void setLastFmApiSecret(std::string_view secret) { _lastFmApiSecret = secret; }
void setLastFmSessionKey(std::string_view key) { _lastFmSessionKey = key; }
// read // read
bool isAdmin() const { return _type == UserType::ADMIN; } bool isAdmin() const { return _type == UserType::ADMIN; }
@@ -134,6 +137,9 @@ namespace lms::db
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; } FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; } ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
std::string_view getListenBrainzToken() const { return _listenbrainzToken; } std::string_view getListenBrainzToken() const { return _listenbrainzToken; }
std::string_view getLastFmApiKey() const { return _lastFmApiKey; }
std::string_view getLastFmApiSecret() const { return _lastFmApiSecret; }
std::string_view getLastFmSessionKey() const { return _lastFmSessionKey; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -155,6 +161,9 @@ namespace lms::db
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend"); Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend"); Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
Wt::Dbo::field(a, _lastFmApiKey, "lastfm_api_key");
Wt::Dbo::field(a, _lastFmApiSecret, "lastfm_api_secret");
Wt::Dbo::field(a, _lastFmSessionKey, "lastfm_session_key");
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _uiStates, Wt::Dbo::ManyToOne, "user"); Wt::Dbo::hasMany(a, _uiStates, Wt::Dbo::ManyToOne, "user");
@@ -176,7 +185,10 @@ namespace lms::db
core::EnumSet<TrackArtistLinkType> _uiInlineArtistRelationships{ TrackArtistLinkType::Composer, TrackArtistLinkType::Performer }; core::EnumSet<TrackArtistLinkType> _uiInlineArtistRelationships{ TrackArtistLinkType::Composer, TrackArtistLinkType::Performer };
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend }; FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend }; ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
std::string _listenbrainzToken; // Musicbrainz Identifier std::string _listenbrainzToken;
std::string _lastFmApiKey;
std::string _lastFmApiSecret;
std::string _lastFmSessionKey;
// Admin defined settings // Admin defined settings
UserType _type{ UserType::REGULAR }; UserType _type{ UserType::REGULAR };
+5 -19
View File
@@ -36,24 +36,15 @@ namespace lms::db::tests
} }
ScopedImage image{ session, "/MyImage" }; ScopedImage image{ session, "/MyImage" };
{
auto transaction{ session.createWriteTransaction() };
image.get().modify()->setAbsoluteFilePath("/tmp/foo");
}
ScopedArtwork artwork{ session, image.lockAndGet() }; ScopedArtwork artwork{ session, image.lockAndGet() };
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artwork::getCount(session), 1); EXPECT_EQ(Artwork::getCount(session), 1);
}
{
auto transaction{ session.createWriteTransaction() };
image.get().modify()->setLastWriteTime(dateTime);
image.get().modify()->setAbsoluteFilePath("/tmp/foo");
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo"); EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo");
} }
} }
@@ -67,20 +58,15 @@ namespace lms::db::tests
ScopedTrackEmbeddedImage image{ session }; ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedArtwork artwork{ session, image.lockAndGet() };
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
session.create<db::TrackEmbeddedImageLink>(track.get(), image.get()); session.create<db::TrackEmbeddedImageLink>(track.get(), image.get());
track.get().modify()->setLastWriteTime(dateTime);
track.get().modify()->setAbsoluteFilePath("/tmp/foo"); track.get().modify()->setAbsoluteFilePath("/tmp/foo");
} }
ScopedArtwork artwork{ session, image.lockAndGet() };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo"); EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo");
} }
} }
+4
View File
@@ -8,10 +8,14 @@ add_executable(test-database
Common.cpp Common.cpp
DatabaseTest.cpp DatabaseTest.cpp
Directory.cpp Directory.cpp
Genre.cpp
Grouping.cpp
Image.cpp Image.cpp
Language.cpp
Listen.cpp Listen.cpp
Medium.cpp Medium.cpp
Migration.cpp Migration.cpp
Mood.cpp
PlayListFile.cpp PlayListFile.cpp
Podcast.cpp Podcast.cpp
RatedArtist.cpp RatedArtist.cpp
+34
View File
@@ -723,4 +723,38 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Cluster_findByTrackAndTypeName)
{
ScopedTrack track{ session };
ScopedClusterType genreType{ session, "GENRE" };
ScopedClusterType moodType{ session, "MOOD" };
ScopedCluster genre1{ session, genreType.lockAndGet(), "Rock" };
ScopedCluster genre2{ session, genreType.lockAndGet(), "Pop" };
ScopedCluster mood{ session, moodType.lockAndGet(), "Happy" };
{
auto transaction{ session.createWriteTransaction() };
genre1.get().modify()->addTrack(track.get());
genre2.get().modify()->addTrack(track.get());
mood.get().modify()->addTrack(track.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto genres{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GENRE")) };
ASSERT_EQ(genres.results.size(), 2);
const auto moods{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("MOOD")) };
ASSERT_EQ(moods.results.size(), 1);
EXPECT_EQ(moods.results.front()->getId(), mood.getId());
const auto all{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId())) };
EXPECT_EQ(all.results.size(), 3);
const auto none{ Cluster::find(session, Cluster::FindParameters{}.setTrack(track.getId()).setClusterTypeName("GROUPING")) };
EXPECT_EQ(none.results.size(), 0);
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
-2
View File
@@ -59,8 +59,6 @@ namespace lms::db::tests
void DatabaseFixture::testDatabaseEmpty() void DatabaseFixture::testDatabaseEmpty()
{ {
using namespace db;
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(session.areAllTablesEmpty()); EXPECT_TRUE(session.areAllTablesEmpty());
+4
View File
@@ -29,8 +29,12 @@
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Genre.hpp"
#include "database/objects/Grouping.hpp"
#include "database/objects/Language.hpp"
#include "database/objects/Listen.hpp" #include "database/objects/Listen.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Mood.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
+233
View File
@@ -0,0 +1,233 @@
/*
* 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 "Common.hpp"
namespace lms::db::tests
{
using ScopedGenre = ScopedEntity<db::Genre>;
TEST_F(DatabaseFixture, Genre_create)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::getCount(session), 0);
}
ScopedGenre genre{ session, "Rock" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::getCount(session), 1);
const Genre::pointer found{ Genre::find(session, genre.getId()) };
ASSERT_TRUE(found);
EXPECT_EQ(found->getName(), "Rock");
EXPECT_EQ(found->getTrackCount(), 0);
EXPECT_EQ(found->getReleaseCount(), 0);
}
}
TEST_F(DatabaseFixture, Genre_findByName)
{
ScopedGenre genre{ session, "Jazz" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Genre::find(session, "Jazz"));
EXPECT_FALSE(Genre::find(session, "jazz"));
EXPECT_FALSE(Genre::find(session, ""));
EXPECT_FALSE(Genre::find(session, "Jaz"));
}
}
TEST_F(DatabaseFixture, Genre_orphan)
{
ScopedGenre genre{ session, "Blues" };
{
auto transaction{ session.createReadTransaction() };
const auto orphans{ Genre::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), genre.getId());
}
}
TEST_F(DatabaseFixture, Genre_singleTrack)
{
ScopedTrack track{ session };
ScopedGenre genre1{ session, "Metal" };
ScopedGenre genre2{ session, "Punk" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 2);
EXPECT_EQ(track->getGenres().size(), 0);
EXPECT_EQ(track->getGenreIds().size(), 0);
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 0);
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setGenres(std::array{ genre1.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(genres.results.size(), 1);
EXPECT_EQ(genres.results.front(), genre1.getId());
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 0);
const auto orphans{ Genre::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), genre2.getId());
const auto trackGenres{ track->getGenres() };
ASSERT_EQ(trackGenres.size(), 1);
EXPECT_EQ(trackGenres.front()->getId(), genre1.getId());
const auto trackGenreIds{ track->getGenreIds() };
ASSERT_EQ(trackGenreIds.size(), 1);
EXPECT_EQ(trackGenreIds.front(), genre1.getId());
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) };
EXPECT_EQ(tracks2.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Genre_multipleGenresOnTrack)
{
ScopedTrack track{ session };
ScopedGenre genre1{ session, "Electronic" };
ScopedGenre genre2{ session, "Ambient" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setGenres(std::array{ genre1.get(), genre2.get() });
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::computeTrackCount(session, genre1.getId()), 1);
EXPECT_EQ(Genre::computeTrackCount(session, genre2.getId()), 1);
EXPECT_EQ(Genre::findOrphanIds(session).results.size(), 0);
const auto trackGenres{ track->getGenres() };
EXPECT_EQ(trackGenres.size(), 2);
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId());
}
}
TEST_F(DatabaseFixture, Genre_computeReleaseCount)
{
ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session };
ScopedGenre genre{ session, "Classical" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
track.get().modify()->setGenres(std::array{ genre.get() });
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Genre::computeTrackCount(session, genre.getId()), 1);
EXPECT_EQ(Genre::computeReleaseCount(session, genre.getId()), 1);
}
}
TEST_F(DatabaseFixture, Genre_nameTruncation)
{
const std::string longName(Genre::maxNameLength + 100, 'x');
const std::string expectedName(Genre::maxNameLength, 'x');
{
auto transaction{ session.createWriteTransaction() };
Genre::pointer genre{ session.create<Genre>(longName) };
ASSERT_TRUE(genre);
EXPECT_EQ(genre->getName().size(), Genre::maxNameLength);
EXPECT_EQ(genre->getName(), expectedName);
genre.remove();
}
}
TEST_F(DatabaseFixture, Genre_sortByName)
{
ScopedGenre g1{ session, "Zzz" };
ScopedGenre g2{ session, "Aaa" };
ScopedGenre g3{ session, "Mmm" };
{
auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::Name)) };
ASSERT_EQ(genres.results.size(), 3);
EXPECT_EQ(genres.results[0], g2.getId());
EXPECT_EQ(genres.results[1], g3.getId());
EXPECT_EQ(genres.results[2], g1.getId());
}
}
TEST_F(DatabaseFixture, Genre_sortByTrackCount)
{
ScopedGenre g1{ session, "Rock" };
ScopedGenre g2{ session, "Jazz" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setGenres(std::array{ g1.get() });
track2.get().modify()->setGenres(std::array{ g1.get() });
track3.get().modify()->setGenres(std::array{ g2.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto genres{ Genre::findIds(session, Genre::FindParameters{}.setSortMethod(GenreSortMethod::TrackCountDesc)) };
ASSERT_EQ(genres.results.size(), 2);
EXPECT_EQ(genres.results[0], g1.getId());
EXPECT_EQ(genres.results[1], g2.getId());
}
}
} // namespace lms::db::tests
+190
View File
@@ -0,0 +1,190 @@
/*
* 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 "Common.hpp"
namespace lms::db::tests
{
using ScopedGrouping = ScopedEntity<db::Grouping>;
TEST_F(DatabaseFixture, Grouping_create)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Grouping::getCount(session), 0);
}
ScopedGrouping grouping{ session, "Soundtrack" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Grouping::getCount(session), 1);
const Grouping::pointer found{ Grouping::find(session, grouping.getId()) };
ASSERT_TRUE(found);
EXPECT_EQ(found->getName(), "Soundtrack");
}
}
TEST_F(DatabaseFixture, Grouping_findByName)
{
ScopedGrouping grouping{ session, "Live" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Grouping::find(session, "Live"));
EXPECT_FALSE(Grouping::find(session, "live"));
EXPECT_FALSE(Grouping::find(session, ""));
EXPECT_FALSE(Grouping::find(session, "Liv"));
}
}
TEST_F(DatabaseFixture, Grouping_orphan)
{
ScopedGrouping grouping{ session, "Podcast" };
{
auto transaction{ session.createReadTransaction() };
const auto orphans{ Grouping::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), grouping.getId());
}
}
TEST_F(DatabaseFixture, Grouping_singleTrack)
{
ScopedTrack track{ session };
ScopedGrouping grouping1{ session, "Audiobook" };
ScopedGrouping grouping2{ session, "Remix" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 2);
EXPECT_EQ(track->getGroupings().size(), 0);
EXPECT_EQ(track->getGroupingIds().size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setGroupings(std::array{ grouping1.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(groupings.results.size(), 1);
EXPECT_EQ(groupings.results.front(), grouping1.getId());
const auto orphans{ Grouping::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), grouping2.getId());
const auto trackGroupings{ track->getGroupings() };
ASSERT_EQ(trackGroupings.size(), 1);
EXPECT_EQ(trackGroupings.front()->getId(), grouping1.getId());
const auto trackGroupingIds{ track->getGroupingIds() };
ASSERT_EQ(trackGroupingIds.size(), 1);
EXPECT_EQ(trackGroupingIds.front(), grouping1.getId());
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) };
EXPECT_EQ(tracks2.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Grouping_multipleGroupingsOnTrack)
{
ScopedTrack track{ session };
ScopedGrouping grouping1{ session, "Soundtrack" };
ScopedGrouping grouping2{ session, "Live" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setGroupings(std::array{ grouping1.get(), grouping2.get() });
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Grouping::findOrphanIds(session).results.size(), 0);
const auto trackGroupings{ track->getGroupings() };
EXPECT_EQ(trackGroupings.size(), 2);
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGrouping(grouping2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId());
}
}
TEST_F(DatabaseFixture, Grouping_sortByName)
{
ScopedGrouping g1{ session, "Zzz" };
ScopedGrouping g2{ session, "Aaa" };
ScopedGrouping g3{ session, "Mmm" };
{
auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::Name)) };
ASSERT_EQ(groupings.results.size(), 3);
EXPECT_EQ(groupings.results[0], g2.getId());
EXPECT_EQ(groupings.results[1], g3.getId());
EXPECT_EQ(groupings.results[2], g1.getId());
}
}
TEST_F(DatabaseFixture, Grouping_sortByTrackCount)
{
ScopedGrouping g1{ session, "Compilation" };
ScopedGrouping g2{ session, "Live" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setGroupings(std::array{ g1.get() });
track2.get().modify()->setGroupings(std::array{ g1.get() });
track3.get().modify()->setGroupings(std::array{ g2.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto groupings{ Grouping::findIds(session, Grouping::FindParameters{}.setSortMethod(GroupingSortMethod::TrackCountDesc)) };
ASSERT_EQ(groupings.results.size(), 2);
EXPECT_EQ(groupings.results[0], g1.getId());
EXPECT_EQ(groupings.results[1], g2.getId());
}
}
} // namespace lms::db::tests
+190
View File
@@ -0,0 +1,190 @@
/*
* 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 "Common.hpp"
namespace lms::db::tests
{
using ScopedLanguage = ScopedEntity<db::Language>;
TEST_F(DatabaseFixture, Language_create)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Language::getCount(session), 0);
}
ScopedLanguage language{ session, "eng" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Language::getCount(session), 1);
const Language::pointer found{ Language::find(session, language.getId()) };
ASSERT_TRUE(found);
EXPECT_EQ(found->getName(), "eng");
}
}
TEST_F(DatabaseFixture, Language_findByName)
{
ScopedLanguage language{ session, "fra" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Language::find(session, "fra"));
EXPECT_FALSE(Language::find(session, "FRA"));
EXPECT_FALSE(Language::find(session, ""));
EXPECT_FALSE(Language::find(session, "fr"));
}
}
TEST_F(DatabaseFixture, Language_orphan)
{
ScopedLanguage language{ session, "jpn" };
{
auto transaction{ session.createReadTransaction() };
const auto orphans{ Language::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), language.getId());
}
}
TEST_F(DatabaseFixture, Language_singleTrack)
{
ScopedTrack track{ session };
ScopedLanguage language1{ session, "eng" };
ScopedLanguage language2{ session, "spa" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Language::findOrphanIds(session).results.size(), 2);
EXPECT_EQ(track->getLanguages().size(), 0);
EXPECT_EQ(track->getLanguageIds().size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setLanguages(std::array{ language1.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(languages.results.size(), 1);
EXPECT_EQ(languages.results.front(), language1.getId());
const auto orphans{ Language::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), language2.getId());
const auto trackLanguages{ track->getLanguages() };
ASSERT_EQ(trackLanguages.size(), 1);
EXPECT_EQ(trackLanguages.front()->getId(), language1.getId());
const auto trackLanguageIds{ track->getLanguageIds() };
ASSERT_EQ(trackLanguageIds.size(), 1);
EXPECT_EQ(trackLanguageIds.front(), language1.getId());
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) };
EXPECT_EQ(tracks2.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Language_multipleLanguagesOnTrack)
{
ScopedTrack track{ session };
ScopedLanguage language1{ session, "eng" };
ScopedLanguage language2{ session, "fra" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setLanguages(std::array{ language1.get(), language2.get() });
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Language::findOrphanIds(session).results.size(), 0);
const auto trackLanguages{ track->getLanguages() };
EXPECT_EQ(trackLanguages.size(), 2);
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setLanguage(language2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId());
}
}
TEST_F(DatabaseFixture, Language_sortByName)
{
ScopedLanguage l1{ session, "zho" };
ScopedLanguage l2{ session, "ara" };
ScopedLanguage l3{ session, "por" };
{
auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::Name)) };
ASSERT_EQ(languages.results.size(), 3);
EXPECT_EQ(languages.results[0], l2.getId());
EXPECT_EQ(languages.results[1], l3.getId());
EXPECT_EQ(languages.results[2], l1.getId());
}
}
TEST_F(DatabaseFixture, Language_sortByTrackCount)
{
ScopedLanguage l1{ session, "eng" };
ScopedLanguage l2{ session, "fra" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setLanguages(std::array{ l1.get() });
track2.get().modify()->setLanguages(std::array{ l1.get() });
track3.get().modify()->setLanguages(std::array{ l2.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto languages{ Language::findIds(session, Language::FindParameters{}.setSortMethod(LanguageSortMethod::TrackCountDesc)) };
ASSERT_EQ(languages.results.size(), 2);
EXPECT_EQ(languages.results[0], l1.getId());
EXPECT_EQ(languages.results[1], l2.getId());
}
}
} // namespace lms::db::tests
+116 -8
View File
@@ -1207,7 +1207,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
EXPECT_FALSE(listen); EXPECT_FALSE(listen);
} }
@@ -1217,7 +1217,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
@@ -1228,7 +1228,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
@@ -1239,12 +1239,69 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3); EXPECT_EQ(listen->getDateTime(), dateTime3);
} }
} }
TEST_F(DatabaseFixture, Listen_getMostRecentRelease_byUserBackend)
{
ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Listen::getMostRecentListen(session, user->getId(), release.getId()));
}
const Wt::WDateTime dateTime1{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1);
}
const Wt::WDateTime dateTime2{ Wt::WDate{ 1999, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1);
}
const Wt::WDateTime dateTime3{ Wt::WDate{ 2001, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), release.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3);
}
{
auto transaction{ session.createWriteTransaction() };
user.get().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Listen::getMostRecentListen(session, user->getId(), release.getId()));
}
}
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
{ {
ScopedTrack track1{ session }; ScopedTrack track1{ session };
@@ -1592,7 +1649,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
EXPECT_FALSE(listen); EXPECT_FALSE(listen);
} }
@@ -1602,7 +1659,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
@@ -1613,7 +1670,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1); EXPECT_EQ(listen->getDateTime(), dateTime1);
} }
@@ -1624,7 +1681,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
EXPECT_TRUE(listen); EXPECT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3); EXPECT_EQ(listen->getDateTime(), dateTime3);
} }
@@ -1698,6 +1755,57 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Listen_getMostRecentTrack_byUserBackend)
{
ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Listen::getMostRecentListen(session, user->getId(), track.getId()));
}
const Wt::WDateTime dateTime1{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1);
}
const Wt::WDateTime dateTime2{ Wt::WDate{ 1999, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime1);
}
const Wt::WDateTime dateTime3{ Wt::WDate{ 2001, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
{
auto transaction{ session.createReadTransaction() };
const auto listen{ Listen::getMostRecentListen(session, user->getId(), track.getId()) };
ASSERT_TRUE(listen);
EXPECT_EQ(listen->getDateTime(), dateTime3);
}
{
auto transaction{ session.createWriteTransaction() };
user.get().modify()->setScrobblingBackend(ScrobblingBackend::ListenBrainz);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Listen::getMostRecentListen(session, user->getId(), track.getId()));
}
}
TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
{ {
ScopedTrack track{ session }; ScopedTrack track{ session };
+17 -4
View File
@@ -20,6 +20,7 @@
#include "Common.hpp" #include "Common.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/UUID.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp" #include "database/objects/ArtistInfo.hpp"
@@ -289,14 +290,14 @@ CREATE INDEX starred_release_user_scrobbler_idx ON starred_release(user_id,scrob
CREATE INDEX starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler);)" }; CREATE INDEX starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler);)" };
const std::string_view createDummyData{ R"( const std::string_view createDummyData{ R"(
-- Inserting artists -- Inserting artists (Artist A has a valid UUID MBID to verify round-trip through migration)
INSERT INTO artist (version, name, sort_name, mbid) VALUES INSERT INTO artist (version, name, sort_name, mbid) VALUES
(1, 'Artist A', 'Artist A', 'mbid_artist_a'), (1, 'Artist A', 'Artist A', '550e8400-e29b-41d4-a716-446655440000'),
(2, 'Artist B', 'Artist B', 'mbid_artist_b'); (2, 'Artist B', 'Artist B', 'mbid_artist_b');
-- Inserting releases -- Inserting releases (Release X has a valid UUID MBID to verify round-trip through migration)
INSERT INTO release (version, name, mbid) VALUES INSERT INTO release (version, name, mbid) VALUES
(1, 'Release X', 'mbid_release_x'), (1, 'Release X', '6ba7b810-9dad-11d1-80b4-00c04fd430c8'),
(2, 'Release Y', 'mbid_release_y'); (2, 'Release Y', 'mbid_release_y');
-- Inserting tracks without any associated artists or releases (Orphan Tracks) -- Inserting tracks without any associated artists or releases (Orphan Tracks)
@@ -381,6 +382,18 @@ VALUES
EXPECT_FALSE(TrackLyrics::find(session, TrackLyricsId{})); EXPECT_FALSE(TrackLyrics::find(session, TrackLyricsId{}));
EXPECT_FALSE(UIState::find(session, UIStateId{})); EXPECT_FALSE(UIState::find(session, UIStateId{}));
EXPECT_FALSE(User::find(session, UserId{})); EXPECT_FALSE(User::find(session, UserId{}));
const auto artistMBID{ core::UUID::fromString("550e8400-e29b-41d4-a716-446655440000") };
ASSERT_TRUE(artistMBID);
const auto artist{ Artist::find(session, *artistMBID) };
ASSERT_TRUE(artist);
EXPECT_EQ(artist->getMBID(), artistMBID);
const auto releaseMBID{ core::UUID::fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8") };
ASSERT_TRUE(releaseMBID);
const auto release{ Release::find(session, *releaseMBID) };
ASSERT_TRUE(release);
EXPECT_EQ(release->getMBID(), releaseMBID);
} }
} }
} // namespace lms::db::tests } // namespace lms::db::tests
+190
View File
@@ -0,0 +1,190 @@
/*
* 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 "Common.hpp"
namespace lms::db::tests
{
using ScopedMood = ScopedEntity<db::Mood>;
TEST_F(DatabaseFixture, Mood_create)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Mood::getCount(session), 0);
}
ScopedMood mood{ session, "Happy" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Mood::getCount(session), 1);
const Mood::pointer found{ Mood::find(session, mood.getId()) };
ASSERT_TRUE(found);
EXPECT_EQ(found->getName(), "Happy");
}
}
TEST_F(DatabaseFixture, Mood_findByName)
{
ScopedMood mood{ session, "Chill" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(Mood::find(session, "Chill"));
EXPECT_FALSE(Mood::find(session, "chill"));
EXPECT_FALSE(Mood::find(session, ""));
EXPECT_FALSE(Mood::find(session, "Chil"));
}
}
TEST_F(DatabaseFixture, Mood_orphan)
{
ScopedMood mood{ session, "Sad" };
{
auto transaction{ session.createReadTransaction() };
const auto orphans{ Mood::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), mood.getId());
}
}
TEST_F(DatabaseFixture, Mood_singleTrack)
{
ScopedTrack track{ session };
ScopedMood mood1{ session, "Dark" };
ScopedMood mood2{ session, "Upbeat" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 2);
EXPECT_EQ(track->getMoods().size(), 0);
EXPECT_EQ(track->getMoodIds().size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setMoods(std::array{ mood1.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setTrack(track.getId())) };
ASSERT_EQ(moods.results.size(), 1);
EXPECT_EQ(moods.results.front(), mood1.getId());
const auto orphans{ Mood::findOrphanIds(session) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results.front(), mood2.getId());
const auto trackMoods{ track->getMoods() };
ASSERT_EQ(trackMoods.size(), 1);
EXPECT_EQ(trackMoods.front()->getId(), mood1.getId());
const auto trackMoodIds{ track->getMoodIds() };
ASSERT_EQ(trackMoodIds.size(), 1);
EXPECT_EQ(trackMoodIds.front(), mood1.getId());
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) };
EXPECT_EQ(tracks2.results.size(), 0);
}
}
TEST_F(DatabaseFixture, Mood_multipleMoodsOnTrack)
{
ScopedTrack track{ session };
ScopedMood mood1{ session, "Melancholic" };
ScopedMood mood2{ session, "Romantic" };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setMoods(std::array{ mood1.get(), mood2.get() });
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Mood::findOrphanIds(session).results.size(), 0);
const auto trackMoods{ track->getMoods() };
EXPECT_EQ(trackMoods.size(), 2);
}
{
auto transaction{ session.createReadTransaction() };
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood1.getId()))) };
ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId());
const auto tracks2{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setMood(mood2.getId()))) };
ASSERT_EQ(tracks2.results.size(), 1);
EXPECT_EQ(tracks2.results.front(), track.getId());
}
}
TEST_F(DatabaseFixture, Mood_sortByName)
{
ScopedMood m1{ session, "Zzz" };
ScopedMood m2{ session, "Aaa" };
ScopedMood m3{ session, "Mmm" };
{
auto transaction{ session.createReadTransaction() };
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::Name)) };
ASSERT_EQ(moods.results.size(), 3);
EXPECT_EQ(moods.results[0], m2.getId());
EXPECT_EQ(moods.results[1], m3.getId());
EXPECT_EQ(moods.results[2], m1.getId());
}
}
TEST_F(DatabaseFixture, Mood_sortByTrackCount)
{
ScopedMood m1{ session, "Energetic" };
ScopedMood m2{ session, "Chill" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setMoods(std::array{ m1.get() });
track2.get().modify()->setMoods(std::array{ m1.get() });
track3.get().modify()->setMoods(std::array{ m2.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto moods{ Mood::findIds(session, Mood::FindParameters{}.setSortMethod(MoodSortMethod::TrackCountDesc)) };
ASSERT_EQ(moods.results.size(), 2);
EXPECT_EQ(moods.results[0], m1.getId());
EXPECT_EQ(moods.results[1], m2.getId());
}
}
} // namespace lms::db::tests
+33
View File
@@ -27,7 +27,11 @@
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedArtwork = ScopedEntity<db::Artwork>; using ScopedArtwork = ScopedEntity<db::Artwork>;
using ScopedGenre = ScopedEntity<db::Genre>;
using ScopedGrouping = ScopedEntity<db::Grouping>;
using ScopedImage = ScopedEntity<db::Image>; using ScopedImage = ScopedEntity<db::Image>;
using ScopedLanguage = ScopedEntity<db::Language>;
using ScopedMood = ScopedEntity<db::Mood>;
TEST_F(DatabaseFixture, Track) TEST_F(DatabaseFixture, Track)
{ {
@@ -590,4 +594,33 @@ namespace lms::db::tests
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{}); EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
} }
} }
TEST_F(DatabaseFixture, Track_combinedTagFilters)
{
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedGenre genre{ session, "Rock" };
ScopedMood mood{ session, "Energetic" };
ScopedGrouping grouping{ session, "Compilation" };
ScopedLanguage language{ session, "eng" };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setGenres(std::array{ genre.get() });
track1.get().modify()->setMoods(std::array{ mood.get() });
track1.get().modify()->setGroupings(std::array{ grouping.get() });
track1.get().modify()->setLanguages(std::array{ language.get() });
track2.get().modify()->setGenres(std::array{ genre.get() });
track2.get().modify()->setMoods(std::array{ mood.get() });
track2.get().modify()->setGroupings(std::array{ grouping.get() });
}
{
auto transaction{ session.createReadTransaction() };
const auto results{ Track::findIds(session, Track::FindParameters{}.setFilters(Filters{}.setGenre(genre.getId()).setMood(mood.getId()).setGrouping(grouping.getId()).setLanguage(language.getId()))) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results.front(), track1.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+114
View File
@@ -275,4 +275,118 @@ namespace lms::db::tests
EXPECT_EQ(links[0]->getTrack()->getId(), track2.getId()); EXPECT_EQ(links[0]->getTrack()->getId(), track2.getId());
} }
} }
TEST_F(DatabaseFixture, Track_getArtists_typeFilterAndOrder)
{
ScopedTrack track{ session };
ScopedArtist artist1{ session, "Artist1" };
ScopedArtist artist2{ session, "Artist2" };
{
auto transaction{ session.createWriteTransaction() };
session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false);
session.create<TrackArtistLink>(track.get(), artist2.get(), TrackArtistLinkType::Artist, false);
session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Mixer, false);
}
{
auto transaction{ session.createReadTransaction() };
const auto artistLinks{ track->getArtists({ TrackArtistLinkType::Artist }) };
ASSERT_EQ(artistLinks.size(), 2);
EXPECT_EQ(artistLinks[0]->getId(), artist1.getId());
EXPECT_EQ(artistLinks[1]->getId(), artist2.getId());
const auto mixerLinks{ track->getArtists({ TrackArtistLinkType::Mixer }) };
ASSERT_EQ(mixerLinks.size(), 1);
EXPECT_EQ(mixerLinks[0]->getId(), artist1.getId());
const auto noFilter{ track->getArtists({}) };
EXPECT_EQ(noFilter.size(), 2);
}
}
TEST_F(DatabaseFixture, Track_getArtistIds_typeFilter)
{
ScopedTrack track{ session };
ScopedArtist artist1{ session, "Artist1" };
ScopedArtist artist2{ session, "Artist2" };
{
auto transaction{ session.createWriteTransaction() };
session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false);
session.create<TrackArtistLink>(track.get(), artist2.get(), TrackArtistLinkType::Artist, false);
session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Mixer, false);
}
{
auto transaction{ session.createReadTransaction() };
const auto artistIds{ track->getArtistIds({ TrackArtistLinkType::Artist }) };
ASSERT_EQ(artistIds.size(), 2);
EXPECT_EQ(artistIds[0], artist1.getId());
EXPECT_EQ(artistIds[1], artist2.getId());
const auto mixerIds{ track->getArtistIds({ TrackArtistLinkType::Mixer }) };
ASSERT_EQ(mixerIds.size(), 1);
EXPECT_EQ(mixerIds[0], artist1.getId());
const auto noFilter{ track->getArtistIds({}) };
EXPECT_EQ(noFilter.size(), 2);
}
}
TEST_F(DatabaseFixture, Track_visitArtistLinks_orderedById)
{
ScopedTrack track{ session };
ScopedArtist artist1{ session, "Artist1" };
ScopedArtist artist2{ session, "Artist2" };
TrackArtistLinkId link1Id;
{
auto transaction{ session.createWriteTransaction() };
auto link1{ session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false) };
link1Id = link1->getId();
session.create<TrackArtistLink>(track.get(), artist2.get(), TrackArtistLinkType::Artist, false);
}
{
auto transaction{ session.createReadTransaction() };
std::vector<db::TrackArtistLink::pointer> links;
track->visitArtistLinks([&](const db::TrackArtistLink::pointer& link) {
links.push_back(link);
});
ASSERT_EQ(links.size(), 2);
EXPECT_EQ(links[0]->getArtistId(), artist1.getId());
EXPECT_EQ(links[1]->getArtistId(), artist2.getId());
}
{
auto transaction{ session.createWriteTransaction() };
TrackArtistLink::find(session, link1Id).remove();
}
{
auto transaction{ session.createWriteTransaction() };
session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false);
}
// artist2's link has lower id, artist1's re-created link has higher id.
{
auto transaction{ session.createReadTransaction() };
std::vector<db::TrackArtistLink::pointer> links;
track->visitArtistLinks([&](const db::TrackArtistLink::pointer& link) {
links.push_back(link);
});
ASSERT_EQ(links.size(), 2);
EXPECT_EQ(links[0]->getArtistId(), artist2.getId());
EXPECT_EQ(links[1]->getArtistId(), artist1.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
@@ -25,7 +25,7 @@ namespace lms::feedback::listenBrainz
{ {
std::ostream& operator<<(std::ostream& os, const Feedback& feedback) std::ostream& operator<<(std::ostream& os, const Feedback& feedback)
{ {
os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score); os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.toString() << "', score = " << static_cast<int>(feedback.score);
return os; return os;
} }
} // namespace lms::feedback::listenBrainz } // namespace lms::feedback::listenBrainz
@@ -127,7 +127,7 @@ namespace lms::feedback::listenBrainz
request.message.addHeader("Authorization", "Token " + listenBrainzToken); request.message.addHeader("Authorization", "Token " + listenBrainzToken);
Wt::Json::Object root; Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value{ std::string{ recordingMBID->getAsString() } }; root["recording_mbid"] = Wt::Json::Value{ recordingMBID->toString() };
root["score"] = Wt::Json::Value{ static_cast<int>(type) }; root["score"] = Wt::Json::Value{ static_cast<int>(type) };
request.message.addBodyText(Wt::Json::serialize(root)); request.message.addBodyText(Wt::Json::serialize(root));
@@ -78,7 +78,7 @@ namespace lms::podcast::utils
std::string generateRandomFileName() std::string generateRandomFileName()
{ {
return std::string{ core::UUID::generate().getAsString() }; return core::UUID::generate().toString();
} }
void removeFile(const std::filesystem::path& filePath) void removeFile(const std::filesystem::path& filePath)
@@ -1,6 +1,6 @@
add_library(lmsrecommendation STATIC add_library(lmsrecommendation STATIC
impl/clusters/ClustersEngine.cpp impl/tags/TagsEngine.cpp
impl/audio-similarity/musicnn/MusicNNEmbeddingEngine.cpp impl/audio-similarity/musicnn/MusicNNEmbeddingEngine.cpp
impl/audio-similarity/musicnn/MusicNNEmbeddingProvider.cpp impl/audio-similarity/musicnn/MusicNNEmbeddingProvider.cpp
impl/track-selection-constraints/SameArtistConstraint.cpp impl/track-selection-constraints/SameArtistConstraint.cpp
@@ -26,7 +26,7 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "audio-similarity/musicnn/MusicNNEmbeddingEngine.hpp" #include "audio-similarity/musicnn/MusicNNEmbeddingEngine.hpp"
#include "clusters/ClustersEngine.hpp" #include "tags/TagsEngine.hpp"
namespace lms::recommendation namespace lms::recommendation
{ {
@@ -57,7 +57,7 @@ namespace lms::recommendation
switch (type) switch (type)
{ {
case db::ScanSettings::RecommendationEngineType::Clusters: case db::ScanSettings::RecommendationEngineType::Clusters:
return std::make_unique<ClusterEngine>(db); return std::make_unique<TagsEngine>(db);
case db::ScanSettings::RecommendationEngineType::AudioSimilarity: case db::ScanSettings::RecommendationEngineType::AudioSimilarity:
return std::make_unique<MusicNNEmbeddingEngine>(db); return std::make_unique<MusicNNEmbeddingEngine>(db);
case db::ScanSettings::RecommendationEngineType::None: case db::ScanSettings::RecommendationEngineType::None:

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