Merge branch 'develop' for release v3.78.0
This commit is contained in:
+1
-1
@@ -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/)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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=" \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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");
|
||||||
|
|||||||
@@ -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") };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user