Merge branch 'develop' for release v3.69.0

This commit is contained in:
emeric
2025-08-09 21:56:11 +02:00
113 changed files with 2371 additions and 954 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12) cmake_minimum_required(VERSION 3.12)
project(lms) project(lms VERSION 3.69.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/)
+3
View File
@@ -90,6 +90,9 @@ If neither exists, it will then search for a file named `artist` (or another nam
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found. 2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder. 3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
## Disc image lookup
_LMS_ automatically associates images with each disc in your collection. Name the image file after the disc's subtitle or another identifier configured in `lms.conf` (see the `medium-image-file-names` setting), and place it in the same directory as the disc's tracks. If no suitable image is found, LMS will also look for embedded images within the tracks of the disc.
## Playlist support ## Playlist support
_LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists. _LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists.
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.About.template">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Admin.About.about}</h5>
</div>
<div class="modal-body">
<div class="container-fluid d-grid gap-2">
<div class="row align-items-center">
<div class="col-4">
${tr:Lms.Admin.About.version}
</div>
<div class="col">
${version}
</div>
</div>
<div class="row align-items-center">
<div class="col-4">
${tr:Lms.Admin.About.homepage}
</div>
<div class="col">
<a href="${homepage-link}" target="_blank" class="dropdown-item link-success">${homepage-link}</a>
</div>
</div>
</div>
</div>
<div class="modal-footer">
${ok-btn class="btn btn-primary"}
</div>
</div>
</div>
</div>
</message>
</messages>
+1 -1
View File
@@ -44,6 +44,7 @@
<li>${scan-settings class="dropdown-item"}</li> <li>${scan-settings class="dropdown-item"}</li>
<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>
${<if-debug-tools>} ${<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>
@@ -57,7 +58,6 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li class="dropdown-header">${username}</li> <li class="dropdown-header">${username}</li>
<li class="dropdown-divider"></li>
<li>${settings class="dropdown-item"}</li> <li>${settings class="dropdown-item"}</li>
<li>${logout class="dropdown-item"}</li> <li>${logout class="dropdown-item"}</li>
</ul> </ul>
+8 -2
View File
@@ -52,12 +52,18 @@
<message id="Lms.Error.user-not-found">User not found</message> <message id="Lms.Error.user-not-found">User not found</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> About</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Debug tools</message> <message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Debug tools</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Libraries</message> <message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Libraries</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Scan settings</message> <message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Scan settings</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message> <message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message> <message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
<!--About-->
<message id="Lms.Admin.About.about">About</message>
<message id="Lms.Admin.About.homepage">Homepage</message>
<message id="Lms.Admin.About.version">Version</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">Music libraries</message> <message id="Lms.Admin.MediaLibraries.media-libraries">Music libraries</message>
<message id="Lms.Admin.MediaLibrary.create-library">Create library</message> <message id="Lms.Admin.MediaLibrary.create-library">Create library</message>
@@ -101,11 +107,11 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message> <message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}</message> <message id="Lms.Admin.ScannerController.bad-embedded-image">Unable to read the image at index {1}: {2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message> <message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</message> <message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file: {1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Cannot parse lyrics file</message> <message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Cannot parse lyrics file</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Cannot parse playlist file</message> <message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Cannot parse playlist file</message>
<message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message> <message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message>
+8 -2
View File
@@ -52,12 +52,18 @@
<message id="Lms.Error.user-not-found">No existe el usuario</message> <message id="Lms.Error.user-not-found">No existe el usuario</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> Acerca de</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Herramientas de depuración</message> <message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Herramientas de depuración</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliotecas</message> <message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliotecas</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Opciones de escaneo</message> <message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Opciones de escaneo</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Escanear</message> <message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Escanear</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Usuarios</message> <message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Usuarios</message>
<!--About-->
<message id="Lms.Admin.About.about">Acerca de</message>
<message id="Lms.Admin.About.homepage">Página principal</message>
<message id="Lms.Admin.About.version">Versión</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">Bibliotecas musicales</message> <message id="Lms.Admin.MediaLibraries.media-libraries">Bibliotecas musicales</message>
<message id="Lms.Admin.MediaLibrary.create-library">Crear una biblioteca</message> <message id="Lms.Admin.MediaLibrary.create-library">Crear una biblioteca</message>
@@ -101,11 +107,11 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message> <message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1}</message> <message id="Lms.Admin.ScannerController.bad-embedded-image">No se puede leer la imagen en el índice {1} : {2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message> <message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen</message> <message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen : {1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message> <message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message> <message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message>
<message id="Lms.Admin.ScannerController.compact">Compactar la base de datos. <strong>¡Atención!:</strong> esta operación puede tardar bastante tiempo y la aplicación se bloqueará mientras dura la operación de compactación</message> <message id="Lms.Admin.ScannerController.compact">Compactar la base de datos. <strong>¡Atención!:</strong> esta operación puede tardar bastante tiempo y la aplicación se bloqueará mientras dura la operación de compactación</message>
+8 -2
View File
@@ -52,12 +52,18 @@
<message id="Lms.Error.user-not-found">L'utilisateur n'existe pas</message> <message id="Lms.Error.user-not-found">L'utilisateur n'existe pas</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> À propos</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Outils de débogage</message> <message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Outils de débogage</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliothèques</message> <message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliothèques</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Paramètres du scan</message> <message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Paramètres du scan</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message> <message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message> <message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
<!--About-->
<message id="Lms.Admin.About.about">À propos</message>
<message id="Lms.Admin.About.homepage">Page daccueil</message>
<message id="Lms.Admin.About.version">Version</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">Bibliothèques musicales</message> <message id="Lms.Admin.MediaLibraries.media-libraries">Bibliothèques musicales</message>
<message id="Lms.Admin.MediaLibrary.create-library">Créer une bibliothèque</message> <message id="Lms.Admin.MediaLibrary.create-library">Créer une bibliothèque</message>
@@ -101,11 +107,11 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message> <message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossible de lire l'image à l'indice {1}</message> <message id="Lms.Admin.ScannerController.bad-embedded-image">Impossible de lire l'image à l'indice {1} : {2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message> <message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</message> <message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image : {1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossible d'analyser le fichier de paroles</message> <message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossible d'analyser le fichier de paroles</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossible d'analyser le fichier de liste de lecture</message> <message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossible d'analyser le fichier de liste de lecture</message>
<message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message> <message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message>
+8 -2
View File
@@ -52,12 +52,18 @@
<message id="Lms.Error.user-not-found">Utente non trovato</message> <message id="Lms.Error.user-not-found">Utente non trovato</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> Informazioni</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Strumenti di debug</message> <message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Strumenti di debug</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Librerie</message> <message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Librerie</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Impostazioni di scansione</message> <message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Impostazioni di scansione</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message> <message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Scanner</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message> <message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message>
<!--About-->
<message id="Lms.Admin.About.about">Informazioni</message>
<message id="Lms.Admin.About.homepage">Pagina iniziale</message>
<message id="Lms.Admin.About.version">Versione</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">Librerie musicali</message> <message id="Lms.Admin.MediaLibraries.media-libraries">Librerie musicali</message>
<message id="Lms.Admin.MediaLibrary.create-library">Crea libreria</message> <message id="Lms.Admin.MediaLibrary.create-library">Crea libreria</message>
@@ -101,11 +107,11 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message> <message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">Impossibile leggere l'immagine all'indice {1}</message> <message id="Lms.Admin.ScannerController.bad-embedded-image">Impossibile leggere l'immagine all'indice {1} : {2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message> <message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</message> <message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine : {1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossibile analizzare il file dei testi</message> <message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Impossibile analizzare il file dei testi</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossibile analizzare il file della playlist</message> <message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Impossibile analizzare il file della playlist</message>
<message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message> <message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message>
+8 -2
View File
@@ -53,12 +53,18 @@
<message id="Lms.Error.user-not-found">Użytkownik nie odnaleziony</message> <message id="Lms.Error.user-not-found">Użytkownik nie odnaleziony</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> O programie</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Narzędzia debugowania</message> <message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> Narzędzia debugowania</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Biblioteki</message> <message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Biblioteki</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Ustawienia skanowania</message> <message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Ustawienia skanowania</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Skaner</message> <message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Skaner</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Użytkownicy</message> <message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Użytkownicy</message>
<!--About-->
<message id="Lms.Admin.About.about">O programie</message>
<message id="Lms.Admin.About.homepage">Strona główna</message>
<message id="Lms.Admin.About.version">Wersja</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">Biblioteki muzyczne</message> <message id="Lms.Admin.MediaLibraries.media-libraries">Biblioteki muzyczne</message>
<message id="Lms.Admin.MediaLibrary.create-library">Utwórz bibliotekę</message> <message id="Lms.Admin.MediaLibrary.create-library">Utwórz bibliotekę</message>
@@ -102,11 +108,11 @@
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message> <message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1}</message> <message id="Lms.Admin.ScannerController.bad-embedded-image">Nie można odczytać obrazu pod indeksem {1} : {2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message> <message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</message> <message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu : {1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Nie można przetworzyć pliku z tekstem</message> <message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">Nie można przetworzyć pliku z tekstem</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Nie można przeanalizować pliku playlisty</message> <message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Nie można przeanalizować pliku playlisty</message>
<message id="Lms.Admin.ScannerController.compact">Sprasuj bazę danych. <strong>Uwaga:</strong> może to trochę zająć, a cała aplikacja będzie w tym czasie zablokowana!</message> <message id="Lms.Admin.ScannerController.compact">Sprasuj bazę danych. <strong>Uwaga:</strong> może to trochę zająć, a cała aplikacja będzie w tym czasie zablokowana!</message>
+6
View File
@@ -58,6 +58,12 @@
<!--About-->
<!--MediaLibraries--> <!--MediaLibraries-->
+2
View File
@@ -19,6 +19,8 @@
<message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message> <message id="Lms.template.edit-btn"><i class="fa fa-fw fa-edit"></i></message>
<message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message> <message id="Lms.template.more-btn"><i class="fa fa-fw fa-ellipsis-v"></i></message>
<message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message> <message id="Lms.template.play-btn"><i class="fa fa-fw fa-play"></i></message>
<message id="Lms.template.star-btn"><i class="fa fa-fw fa-star-o"></i></message>
<message id="Lms.template.toggle-visibility-btn"><i class="fa fa-fw fa-eye"></i></message> <message id="Lms.template.toggle-visibility-btn"><i class="fa fa-fw fa-eye"></i></message>
<message id="Lms.template.unstar-btn"><i class="fa fa-fw fa-star"></i></message>
</messages> </messages>
+2 -1
View File
@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
${container class="d-grid gap-3"} ${disc-container class="d-grid gap-3"}
${<if-has-copyright>} ${<if-has-copyright>}
<div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div> <div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div>
${</if-has-copyright>} ${</if-has-copyright>}
@@ -101,6 +101,7 @@
</div> </div>
<div class="p-2 d-flex"> <div class="p-2 d-flex">
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
${star-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"} ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}"> <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
+1
View File
@@ -51,6 +51,7 @@
</div> </div>
<div class="p-2 d-flex"> <div class="p-2 d-flex">
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"} ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
${star-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"} ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}"> <ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
+7 -4
View File
@@ -7,9 +7,8 @@ working-dir = "/var/lms";
# ffmpeg location # ffmpeg location
ffmpeg-file = "/usr/bin/ffmpeg"; ffmpeg-file = "/usr/bin/ffmpeg";
# Log files, empty means stdout # Log files, empty means debug+info on stdout, warning+error+fatal on stderr
log-file = ""; log-file = "";
access-log-file = "";
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal" # Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
# "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application # "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application
log-min-severity = "info"; log-min-severity = "info";
@@ -99,13 +98,17 @@ cover-max-cache-size = 30;
# JPEG quality for covers (range is 1-100) # JPEG quality for covers (range is 1-100)
cover-jpeg-quality = 75; cover-jpeg-quality = 75;
# Preferred file names for covers (order is important) # Preferred file names for covers (order is important, accept wildcards)
cover-preferred-file-names = ("cover", "front", "folder", "default"); cover-preferred-file-names = ("cover", "front", "folder", "default");
# File names for artist images (order is important) # File names for artist images (order is important, accept wildcards)
# Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries. # Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries.
artist-image-file-names = ("artist"); artist-image-file-names = ("artist");
# File names for medium images (order is important, accept wildcards)
# Note: files named after the disc itself are always searched before the names in this list.
medium-image-file-names = ("discsubtitle");
# Playqueue max entry count # Playqueue max entry count
playqueue-max-entry-count = 1000; playqueue-max-entry-count = 1000;
+9 -2
View File
@@ -2,6 +2,14 @@ pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive) pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash) pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
set(LMS_VERSION ${PROJECT_VERSION})
configure_file(
impl/Version.cpp.in
impl/Version.cpp
@ONLY
)
add_library(lmscore STATIC add_library(lmscore STATIC
impl/http/Client.cpp impl/http/Client.cpp
impl/http/SendQueue.cpp impl/http/SendQueue.cpp
@@ -19,12 +27,11 @@ add_library(lmscore STATIC
impl/Path.cpp impl/Path.cpp
impl/Random.cpp impl/Random.cpp
impl/RecursiveSharedMutex.cpp impl/RecursiveSharedMutex.cpp
impl/StreamLogger.cpp
impl/String.cpp impl/String.cpp
impl/TraceLogger.cpp impl/TraceLogger.cpp
impl/UUID.cpp impl/UUID.cpp
impl/WtLogger.cpp
impl/XxHash3.cpp impl/XxHash3.cpp
${CMAKE_CURRENT_BINARY_DIR}/impl/Version.cpp
) )
target_include_directories(lmscore INTERFACE target_include_directories(lmscore INTERFACE
+1 -3
View File
@@ -17,19 +17,17 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <iostream>
#include <thread> #include <thread>
#include <benchmark/benchmark.h> #include <benchmark/benchmark.h>
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/StreamLogger.hpp"
namespace lms::core namespace lms::core
{ {
// The trace logger is meant to built/destroyed once // The trace logger is meant to built/destroyed once
const Service<logging::ILogger> logger{ std::make_unique<logging::StreamLogger>(std::cout, logging::StreamLogger::allSeverities) }; const Service<logging::ILogger> logger{ logging::createLogger() };
const Service<tracing::ITraceLogger> traceLogger{ tracing::createTraceLogger(tracing::Level::Overview) }; const Service<tracing::ITraceLogger> traceLogger{ tracing::createTraceLogger(tracing::Level::Overview) };
static void BM_TraceLogger_Overview(benchmark::State& state) static void BM_TraceLogger_Overview(benchmark::State& state)
+103 -1
View File
@@ -17,7 +17,17 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "core/ILogger.hpp" #include "Logger.hpp"
#include <Wt/WDateTime.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <thread>
#include "core/Exception.hpp"
#include "core/String.hpp"
namespace lms::core::logging namespace lms::core::logging
{ {
@@ -63,6 +73,8 @@ namespace lms::core::logging
return "UI"; return "UI";
case Module::UTILS: case Module::UTILS:
return "UTILS"; return "UTILS";
case Module::WT:
return "WT";
} }
return ""; return "";
} }
@@ -103,4 +115,94 @@ namespace lms::core::logging
{ {
return _oss.str(); return _oss.str();
} }
std::unique_ptr<ILogger> createLogger(Severity minSeverity, const std::filesystem::path& logFilePath)
{
return std::make_unique<Logger>(minSeverity, logFilePath);
}
Logger::OutputStream::OutputStream(std::ostream& os)
: stream{ os }
{
}
Logger::Logger(Severity minSeverity, const std::filesystem::path& logFilePath)
{
if (!logFilePath.empty())
{
_logFileStream = std::make_unique<std::ofstream>(logFilePath, std::ios::out | std::ios::app);
if (!_logFileStream->is_open())
{
const std::error_code ec{ errno, std::generic_category() };
throw LmsException{ "Cannot open log file '" + logFilePath.string() + "' for writing: " + ec.message() };
}
}
switch (minSeverity)
{
case core::logging::Severity::DEBUG:
if (_logFileStream)
addOutputStream(*_logFileStream, { core::logging::Severity::DEBUG });
else
addOutputStream(std::cout, { core::logging::Severity::DEBUG });
[[fallthrough]];
case core::logging::Severity::INFO:
if (_logFileStream)
addOutputStream(*_logFileStream, { core::logging::Severity::INFO });
else
addOutputStream(std::cout, { core::logging::Severity::INFO });
[[fallthrough]];
case core::logging::Severity::WARNING:
if (_logFileStream)
addOutputStream(*_logFileStream, { core::logging::Severity::WARNING });
else
addOutputStream(std::cerr, { core::logging::Severity::WARNING });
[[fallthrough]];
case core::logging::Severity::ERROR:
if (_logFileStream)
addOutputStream(*_logFileStream, { core::logging::Severity::ERROR });
else
addOutputStream(std::cerr, { core::logging::Severity::ERROR });
[[fallthrough]];
case core::logging::Severity::FATAL:
if (_logFileStream)
addOutputStream(*_logFileStream, { core::logging::Severity::FATAL });
else
addOutputStream(std::cerr, { core::logging::Severity::FATAL });
break;
}
}
Logger::~Logger() = default;
void Logger::addOutputStream(std::ostream& os, Severity severity)
{
auto it{ std::find_if(_outputStreams.begin(), _outputStreams.end(), [&os](const OutputStream& outputStream) { return &outputStream.stream == &os; }) };
if (it == _outputStreams.end())
it = _outputStreams.emplace(_outputStreams.end(), os);
assert(!_severityToOutputStreamMap.contains(severity));
_severityToOutputStreamMap.emplace(severity, &(*it));
}
bool Logger::isSeverityActive(Severity severity) const
{
return _severityToOutputStreamMap.contains(severity);
}
void Logger::processLog(const Log& log)
{
processLog(log.getModule(), log.getSeverity(), log.getMessage());
}
void Logger::processLog(Module module, Severity severity, std::string_view message)
{
assert(isSeverityActive(severity)); // should have been filtered out by a isSeverityActive call
OutputStream* outputStream{ _severityToOutputStreamMap.at(severity) };
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
std::unique_lock lock{ outputStream->mutex };
outputStream->stream << stringUtils::toISO8601String(now) << " " << std::this_thread::get_id() << " [" << getSeverityName(severity) << "] [" << getModuleName(module) << "] " << message << std::endl;
}
} // namespace lms::core::logging } // namespace lms::core::logging
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2019 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 <filesystem>
#include <iosfwd>
#include <list>
#include <mutex>
#include <unordered_map>
#include "core/ILogger.hpp"
namespace lms::core::logging
{
class Logger final : public ILogger
{
public:
Logger(Severity minSeverity, const std::filesystem::path& logFilePath);
~Logger() override;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
bool isSeverityActive(Severity severity) const override;
void processLog(const Log& log) override;
void processLog(Module module, Severity severity, std::string_view message) override;
void addOutputStream(std::ostream& os, Severity severity);
struct OutputStream
{
OutputStream(std::ostream& os);
std::mutex mutex;
std::ostream& stream;
};
std::list<OutputStream> _outputStreams;
std::unordered_map<Severity, OutputStream*> _severityToOutputStreamMap;
std::unique_ptr<std::ofstream> _logFileStream;
};
} // namespace lms::core::logging
+20
View File
@@ -86,4 +86,24 @@ namespace lms::core::pathUtils
return longestCommonPath; return longestCommonPath;
} }
std::string sanitizeFileStem(const std::string_view fileStem)
{
// Keep UTF8-encoded characters, but skip illegal ASCII characters
constexpr std::array<unsigned char, 9> illegalChars{ '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
static_assert(std::all_of(std::begin(illegalChars), std::end(illegalChars), [](unsigned char c) { return c < 128; }), "Illegal characters must be ASCII");
std::string sanitized;
sanitized.reserve(fileStem.size());
for (const char c : fileStem)
{
if (std::any_of(std::begin(illegalChars), std::end(illegalChars), [c](char illegalChar) { return c == illegalChar; }))
continue;
sanitized.push_back(c);
}
return sanitized;
}
} // namespace lms::core::pathUtils } // namespace lms::core::pathUtils
+1
View File
@@ -25,6 +25,7 @@
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/String.hpp"
namespace lms::core::tracing namespace lms::core::tracing
{ {
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Emeric Poupon * Copyright (C) 2025 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,22 +17,12 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <cassert> #include "core/Version.hpp"
#include <thread>
#include "core/StreamLogger.hpp" namespace lms::core
namespace lms::core::logging
{ {
StreamLogger::StreamLogger(std::ostream& os, EnumSet<Severity> severities) std::string_view getVersion()
: _os{ os }
, _severities{ severities }
{ {
return "v" "@LMS_VERSION@";
} }
} // namespace lms::core
void StreamLogger::processLog(const Log& log)
{
assert(isSeverityActive(log.getSeverity()));
_os << std::this_thread::get_id() << " [" << getSeverityName(log.getSeverity()) << "] [" << getModuleName(log.getModule()) << "] " << log.getMessage() << std::endl;
}
} // namespace lms::core::logging
-75
View File
@@ -1,75 +0,0 @@
/*
* Copyright (C) 2019 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/WtLogger.hpp"
#include <sstream>
#include <thread>
#include <Wt/WLogger.h>
#include <Wt/WServer.h>
#include "core/Exception.hpp"
namespace lms::core::logging
{
namespace
{
std::string to_string(std::thread::id id)
{
std::ostringstream oss;
oss << id;
return oss.str();
}
} // namespace
WtLogger::WtLogger(Severity minSeverity)
: _minSeverity{ minSeverity }
{
}
std::string WtLogger::computeLogConfig(Severity minSeverity)
{
switch (minSeverity)
{
case Severity::DEBUG:
return "*";
case Severity::INFO:
return "* -debug";
case Severity::WARNING:
return "* -debug -info";
case Severity::ERROR:
return "* -debug -info -warning";
case Severity::FATAL:
return "* -debug -info -warning -error";
}
throw LmsException{ "Unhandled severity" };
}
bool WtLogger::isSeverityActive(Severity severity) const
{
return static_cast<int>(severity) <= static_cast<int>(_minSeverity);
}
void WtLogger::processLog(const Log& log)
{
Wt::log(getSeverityName(log.getSeverity())) << Wt::WLogger::sep << to_string(std::this_thread::get_id()) << Wt::WLogger::sep << "[" << getModuleName(log.getModule()) << "]" << Wt::WLogger::sep << log.getMessage();
}
} // namespace lms::core::logging
+7 -1
View File
@@ -19,11 +19,12 @@
#pragma once #pragma once
#include <filesystem>
#include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp"
namespace lms::core::logging namespace lms::core::logging
{ {
@@ -57,6 +58,7 @@ namespace lms::core::logging
TRANSCODING, TRANSCODING,
UI, UI,
UTILS, UTILS,
WT,
}; };
const char* getModuleName(Module mod); const char* getModuleName(Module mod);
@@ -92,7 +94,11 @@ namespace lms::core::logging
virtual bool isSeverityActive(Severity severity) const = 0; virtual bool isSeverityActive(Severity severity) const = 0;
virtual void processLog(const Log& log) = 0; virtual void processLog(const Log& log) = 0;
virtual void processLog(Module module, Severity severity, std::string_view message) = 0;
}; };
static constexpr Severity defaultMinSeverity{ Severity::INFO };
std::unique_ptr<ILogger> createLogger(Severity minSeverity = defaultMinSeverity, const std::filesystem::path& logFilePath = {});
} // namespace lms::core::logging } // namespace lms::core::logging
#define LMS_LOG(module, severity, message) \ #define LMS_LOG(module, severity, message) \
+5
View File
@@ -21,6 +21,8 @@
#include <filesystem> #include <filesystem>
#include <span> #include <span>
#include <string>
#include <string_view>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -54,4 +56,7 @@ namespace lms::core::pathUtils
return longestCommonPath; return longestCommonPath;
} }
// A method that sanitize a file stem, removing any illegal chars
std::string sanitizeFileStem(std::string_view fileStem);
} // namespace lms::core::pathUtils } // namespace lms::core::pathUtils
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2019 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 "core/EnumSet.hpp"
#include "core/ILogger.hpp"
namespace lms::core::logging
{
class StreamLogger final : public ILogger
{
public:
static constexpr EnumSet<Severity> allSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO, Severity::DEBUG };
static constexpr EnumSet<Severity> defaultSeverities{ Severity::FATAL, Severity::ERROR, Severity::WARNING, Severity::INFO };
StreamLogger(std::ostream& oss, EnumSet<Severity> severities = defaultSeverities);
bool isSeverityActive(Severity severity) const override { return _severities.contains(severity); }
void processLog(const Log& log) override;
private:
std::ostream& _os;
const EnumSet<Severity> _severities;
};
} // namespace lms::core::logging
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Emeric Poupon * Copyright (C) 2025 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,22 +19,9 @@
#pragma once #pragma once
#include <string> #include <string_view>
#include "core/ILogger.hpp" namespace lms::core
namespace lms::core::logging
{ {
class WtLogger final : public ILogger std::string_view getVersion();
{ } // namespace lms::core
public:
WtLogger(Severity minSeverity);
static std::string computeLogConfig(Severity minSeverity);
private:
bool isSeverityActive(Severity severity) const override;
void processLog(const Log& log) override;
const Severity _minSeverity;
};
} // namespace lms::core::logging
+30
View File
@@ -103,4 +103,34 @@ namespace lms::core::pathUtils::tests
EXPECT_EQ(core::pathUtils::isPathInRootPath(test.path, test.rootPath), test.expectedResult) << "Failed: path = " << test.path << ", rootPath = " << test.rootPath; EXPECT_EQ(core::pathUtils::isPathInRootPath(test.path, test.rootPath), test.expectedResult) << "Failed: path = " << test.path << ", rootPath = " << test.rootPath;
} }
} }
TEST(Path, sanitizeFileStem)
{
struct TestCase
{
std::string input;
std::string_view expectedOutput;
};
TestCase tests[]{
{ "", "" }, // empty input
{ "valid_file_name", "valid_file_name" },
{ "invalid:file*name?", "invalidfilename" },
{ "another|invalid<name>", "anotherinvalidname" },
{ "/leading/slash", "leadingslash" },
{ "\\backslash\\file", "backslashfile" },
{ "file_with_äöüß", "file_with_äöüß" }, // keep German umlauts
{ "file_with_éèêë", "file_with_éèêë" }, // keep French accents
{ "héllo 漢字", "héllo 漢字" }, // keep UTF8 characters
{ "file_with_üñîçødë", "file_with_üñîçødë" }, // keep special characters
{ "file_with_!@#$%^&*()_+", "file_with_!@#$%^&()_+" }, // remove special characters
{ "file_with_", "file_with_" }, // handle double dots
{ "file.with.extension", "file.with.extension" }, // keep extensions
};
for (const TestCase& test : tests)
{
EXPECT_EQ(core::pathUtils::sanitizeFileStem(test.input), test.expectedOutput) << "Failed: input = " << test.input;
}
}
} // namespace lms::core::pathUtils::tests } // namespace lms::core::pathUtils::tests
+1
View File
@@ -8,6 +8,7 @@ add_library(lmsdatabase STATIC
impl/objects/Image.cpp impl/objects/Image.cpp
impl/objects/Listen.cpp impl/objects/Listen.cpp
impl/objects/MediaLibrary.cpp impl/objects/MediaLibrary.cpp
impl/objects/Medium.cpp
impl/objects/PlayListFile.cpp impl/objects/PlayListFile.cpp
impl/objects/PlayQueue.cpp impl/objects/PlayQueue.cpp
impl/objects/TrackArtistLink.cpp impl/objects/TrackArtistLink.cpp
+107 -1
View File
@@ -34,7 +34,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 98 }; static constexpr Version LMS_DATABASE_VERSION{ 99 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -1423,6 +1423,111 @@ WHERE art.image_id IS NULL)");
dropIndexes(session); dropIndexes(session);
} }
void migrateFromV98(Session& session)
{
// Medium support
dropIndexes(session);
// New table for mediums
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "medium" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"position" integer,
"track_count" integer,
"media" text not null,
"replay_gain" real,
"release_id" bigint,
"preferred_artwork_id" bigint,
constraint "fk_medium_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_medium_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
))");
// New primary key in track -> need to recreate the table
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"scan_version" integer not null,
"track_number" integer,
"name" text not null,
"duration" integer,
"bitrate" integer not null,
"bits_per_sample" integer not null,
"channel_count" integer not null,
"sample_rate" integer not null,
"date" text,
"original_date" text,
"absolute_file_path" text not null,
"file_size" bigint not null,
"file_last_write" text,
"file_added" text,
"mbid" text not null,
"recording_mbid" text not null,
"copyright" text not null,
"copyright_url" text not null,
"advisory" integer not null,
"replay_gain" real,
"artist_display_name" text not null,
"comment" text not null,
"medium_id" bigint,
"release_id" bigint,
"media_library_id" bigint,
"directory_id" bigint,
"preferred_artwork_id" bigint,
"preferred_media_artwork_id" bigint,
constraint "fk_track_medium" foreign key ("medium_id") references "medium" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred,
constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
))");
// Migrate data:
// - removed disc_number, total_track, disc_subtitle, release_replay_gain
// - renamed track_replay_gain to replay_gain
// - with the new medium_id field set to null
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
SELECT
id,
version,
scan_version,
track_number,
name,
duration,
bitrate,
bits_per_sample,
channel_count,
sample_rate,
date,
original_date,
absolute_file_path,
file_size,
file_last_write,
file_added,
mbid,
recording_mbid,
copyright,
copyright_url,
advisory,
track_replay_gain AS replay_gain,
artist_display_name,
comment,
NULL as medium_id,
release_id,
media_library_id,
directory_id,
preferred_artwork_id,
preferred_media_artwork_id
FROM track)");
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
// Just increment the scan version of the settings to make the next scan rescan all audio files
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
}
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)" };
@@ -1497,6 +1602,7 @@ WHERE art.image_id IS NULL)");
{ 95, migrateFromV95 }, { 95, migrateFromV95 },
{ 96, migrateFromV96 }, { 96, migrateFromV96 },
{ 97, migrateFromV97 }, { 97, migrateFromV97 },
{ 98, migrateFromV98 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+5 -1
View File
@@ -33,6 +33,7 @@
#include "database/objects/Image.hpp" #include "database/objects/Image.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/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/PlayQueue.hpp" #include "database/objects/PlayQueue.hpp"
#include "database/objects/RatedArtist.hpp" #include "database/objects/RatedArtist.hpp"
@@ -82,6 +83,7 @@ namespace lms::db
_session.mapClass<Label>("label"); _session.mapClass<Label>("label");
_session.mapClass<Listen>("listen"); _session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library"); _session.mapClass<MediaLibrary>("media_library");
_session.mapClass<Medium>("medium");
_session.mapClass<PlayListFile>("playlist_file"); _session.mapClass<PlayListFile>("playlist_file");
_session.mapClass<PlayQueue>("playqueue"); _session.mapClass<PlayQueue>("playqueue");
_session.mapClass<RatedArtist>("rated_artist"); _session.mapClass<RatedArtist>("rated_artist");
@@ -235,6 +237,8 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)"); 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_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_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 playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)");
@@ -262,12 +266,12 @@ namespace lms::db
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_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_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_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_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_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_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_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_recording_mbid_idx ON track(recording_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_disc_idx ON track(release_id, disc_number)");
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_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_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 track_release_file_added_idx ON track(release_id, file_added)");
+1 -1
View File
@@ -23,7 +23,7 @@
namespace lms::db::utils namespace lms::db::utils
{ {
std::string escapeLikeKeyword(std::string_view keyword) std::string escapeForLikeKeyword(std::string_view keyword)
{ {
return core::stringUtils::escapeString(keyword, "%_", escapeChar); return core::stringUtils::escapeString(keyword, "%_", escapeChar);
} }
+1 -1
View File
@@ -38,7 +38,7 @@ namespace lms::db::utils
{ {
#define ESCAPE_CHAR_STR "\\" #define ESCAPE_CHAR_STR "\\"
static inline constexpr char escapeChar{ '\\' }; static inline constexpr char escapeChar{ '\\' };
std::string escapeLikeKeyword(std::string_view keywords); std::string escapeForLikeKeyword(std::string_view keywords);
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
+2 -2
View File
@@ -106,13 +106,13 @@ namespace lms::db
for (const std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
} }
for (const std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
} }
query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")"); query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")");
@@ -26,6 +26,7 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.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"
+7 -3
View File
@@ -40,10 +40,11 @@ namespace lms::db
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (params.artist.isValid() if (params.artist.isValid()
|| params.release.isValid()) || params.release.isValid()
|| params.medium.isValid())
{ {
query.join("track t ON t.directory_id = d.id"); query.join("track t ON t.directory_id = d.id");
query.groupBy("d.id"); query.groupBy("d.id");
@@ -55,6 +56,9 @@ namespace lms::db
if (params.parentDirectory.isValid()) if (params.parentDirectory.isValid())
query.where("d.parent_directory_id = ?").bind(params.parentDirectory); query.where("d.parent_directory_id = ?").bind(params.parentDirectory);
if (params.medium.isValid())
query.where("t.medium_id = ?").bind(params.medium);
if (params.release.isValid()) if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
@@ -91,7 +95,7 @@ namespace lms::db
case DirectorySortMethod::None: case DirectorySortMethod::None:
break; break;
case DirectorySortMethod::Name: case DirectorySortMethod::Name:
query.orderBy("name COLLATE NOCASE"); query.orderBy("d.name COLLATE NOCASE");
break; break;
} }
+14 -1
View File
@@ -43,7 +43,20 @@ namespace lms::db
if (params.directory.isValid()) if (params.directory.isValid())
query.where("i.directory_id = ?").bind(params.directory); query.where("i.directory_id = ?").bind(params.directory);
if (!params.fileStem.empty()) if (!params.fileStem.empty())
query.where("i.stem = ? COLLATE NOCASE").bind(params.fileStem); {
// if contains a wildcard, replace by % for LIKE
if (params.processWildcardsInFileStem.value() && params.fileStem.find('*') != std::string::npos)
{
std::string fileStem{ params.fileStem };
utils::escapeForLikeKeyword(fileStem);
std::replace(std::begin(fileStem), std::end(fileStem), '*', '%');
query.where("i.stem LIKE ? COLLATE NOCASE ESCAPE '" ESCAPE_CHAR_STR "'").bind(fileStem);
}
else
{
query.where("i.stem = ? COLLATE NOCASE").bind(params.fileStem);
}
}
return query; return query;
} }
+4 -4
View File
@@ -100,13 +100,13 @@ namespace lms::db
for (const std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
} }
for (const std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
} }
query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")"); query.where("(" + core::stringUtils::joinStrings(clauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(sortClauses, " AND ") + ")");
@@ -169,7 +169,7 @@ namespace lms::db
} }
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
return query; return query;
} }
@@ -227,7 +227,7 @@ namespace lms::db
} }
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
return query; return query;
} }
+128
View File
@@ -0,0 +1,128 @@
/*
* 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/Medium.hpp"
#include <Wt/Dbo/Impl.h>
#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/MediaLibrary.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"
DBO_INSTANTIATE_TEMPLATES(lms::db::Medium);
namespace lms::db
{
Medium::Medium(ObjectPtr<Release> release)
: _release(getDboPtr(release))
{
}
Medium::pointer Medium::create(Session& session, ObjectPtr<Release> release)
{
return session.getDboSession()->add(std::unique_ptr<Medium>{ new Medium{ release } });
}
std::size_t Medium::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM medium"));
}
Medium::pointer Medium::find(Session& session, MediumId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m").where("m.id = ?").bind(id));
}
Medium::pointer Medium::find(Session& session, ReleaseId releaseId, std::optional<std::size_t> position)
{
session.checkReadTransaction();
std::optional<int> dbPosition;
if (position)
dbPosition = static_cast<int>(*position);
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
query.where("m.release_id = ?").bind(releaseId);
if (position)
query.where("m.position = ?").bind(*dbPosition);
else
query.where("m.position IS NULL");
return utils::fetchQuerySingleResult(query);
}
void Medium::find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func)
{
assert(idRange.isValid());
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m").orderBy("m.id").where("m.id BETWEEN ? AND ?").bind(idRange.first).bind(idRange.last) };
utils::forEachQueryResult(query, [&](const Medium::pointer& medium) {
func(medium);
});
}
IdRange<MediumId> Medium::findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<std::tuple<MediumId, MediumId>>("SELECT MIN(sub.id) AS first_id, MAX(sub.id) AS last_id FROM (SELECT m.id FROM medium m WHERE m.id > ? ORDER BY m.id LIMIT ?) sub") };
query.bind(lastRetrievedId);
query.bind(static_cast<int>(count));
auto res{ utils::fetchQuerySingleResult(query) };
return IdRange<MediumId>{ .first = std::get<0>(res), .last = std::get<1>(res) };
}
RangeResults<MediumId> Medium::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
// select the mediums that have no track
auto query{ session.getDboSession()->query<MediumId>("select m.id from medium m LEFT OUTER JOIN track t ON m.id = t.medium_id WHERE t.id IS NULL") };
return utils::execRangeQuery<MediumId>(query, range);
}
void Medium::updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId)
{
session.checkWriteTransaction();
if (artworkId.isValid())
utils::executeCommand(*session.getDboSession(), "UPDATE medium SET preferred_artwork_id = ? WHERE id = ?", artworkId, mediumId);
else
utils::executeCommand(*session.getDboSession(), "UPDATE medium SET preferred_artwork_id = NULL WHERE id = ?", mediumId);
}
} // namespace lms::db
@@ -28,6 +28,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.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"
+14 -28
View File
@@ -30,6 +30,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -124,7 +125,7 @@ namespace lms::db
query.where("r.name = ?").bind(params.name); query.where("r.name = ?").bind(params.name);
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (params.starringUser.isValid()) if (params.starringUser.isValid())
{ {
@@ -577,28 +578,6 @@ namespace lms::db
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params)); return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params));
} }
std::size_t Release::getDiscCount() const
{
assert(session());
int res{ utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t").where("t.release_id = ?").bind(getId())) };
return res;
}
std::vector<DiscInfo> Release::getDiscs() const
{
assert(session());
using ResultType = std::tuple<int, std::string>;
const auto query{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t").where("t.release_id = ?").bind(getId()).orderBy("disc_number") };
std::vector<DiscInfo> discs;
utils::forEachQueryResult(query, [&](ResultType&& res) {
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::move(std::get<std::string>(res)) });
});
return discs;
}
core::PartialDateTime Release::getDate() const core::PartialDateTime Release::getDate() const
{ {
return getDate(false); return getDate(false);
@@ -753,6 +732,18 @@ namespace lms::db
return _preferredArtwork.id(); return _preferredArtwork.id();
} }
std::vector<ObjectPtr<Medium>> Release::getMediums() const
{
assert(session());
// Select the similar releases using the 5 most used clusters of the release
auto query{ session()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
query.where("m.release_id = ?").bind(getId());
query.orderBy("m.position");
return utils::fetchQueryResults<Medium::pointer>(query);
}
void Release::clearLabels() void Release::clearLabels()
{ {
_labels.clear(); _labels.clear();
@@ -794,11 +785,6 @@ namespace lms::db
return getArtists().size() > 1; return getArtists().size() > 1;
} }
bool Release::hasDiscSubtitle() const
{
return utils::fetchQuerySingleResult(session()->query<int>("SELECT EXISTS (SELECT 1 FROM track WHERE disc_subtitle IS NOT NULL AND disc_subtitle <> '' AND release_id = ?)").bind(getId()));
}
std::size_t Release::getTrackCount() const std::size_t Release::getTrackCount() const
{ {
assert(session()); assert(session());
+18 -7
View File
@@ -24,11 +24,13 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#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/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.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"
@@ -59,7 +61,7 @@ namespace lms::db
assert(params.keywords.empty() || params.name.empty()); assert(params.keywords.empty() || params.name.empty());
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (!params.name.empty()) if (!params.name.empty())
query.where("t.name = ?").bind(params.name); query.where("t.name = ?").bind(params.name);
@@ -146,6 +148,9 @@ namespace lms::db
query.where("r.name = ?").bind(params.releaseName); query.where("r.name = ?").bind(params.releaseName);
} }
if (params.medium.isValid())
query.where("t.medium_id = ?").bind(params.medium);
if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList) if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList)
{ {
query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id"); query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
@@ -156,8 +161,11 @@ namespace lms::db
if (params.trackNumber) if (params.trackNumber)
query.where("t.track_number = ?").bind(*params.trackNumber); query.where("t.track_number = ?").bind(*params.trackNumber);
if (params.discNumber) if (params.sortMethod == TrackSortMethod::DateDescAndRelease
query.where("t.disc_number = ?").bind(*params.discNumber); || params.sortMethod == TrackSortMethod::Release)
{
query.join("medium m ON t.medium_id = m.id");
}
if (params.filters.mediaLibrary.isValid()) if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
@@ -213,16 +221,19 @@ namespace lms::db
query.orderBy("t.absolute_file_path COLLATE NOCASE"); query.orderBy("t.absolute_file_path COLLATE NOCASE");
break; break;
case TrackSortMethod::DateDescAndRelease: case TrackSortMethod::DateDescAndRelease:
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number"); query.orderBy("t.date DESC,t.release_id,m.position,t.track_number");
break; break;
case TrackSortMethod::Release: case TrackSortMethod::Release:
query.orderBy("t.disc_number,t.track_number"); query.orderBy("m.position,t.track_number");
break; break;
case TrackSortMethod::TrackList: case TrackSortMethod::TrackList:
assert(params.trackList.isValid()); assert(params.trackList.isValid());
query.orderBy("t_l_e.id"); query.orderBy("t_l_e.id");
break;
case TrackSortMethod::TrackNumber:
query.orderBy("t.track_number");
break;
} }
return query; return query;
} }
@@ -360,7 +371,7 @@ namespace lms::db
{ {
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.disc_number,track.track_number,track.mbid") }; 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") };
return utils::execRangeQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
@@ -43,8 +43,7 @@ namespace lms::db
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
if (params.artist.isValid() if (params.medium.isValid()
|| params.discNumber.has_value()
|| params.track.isValid() || params.track.isValid()
|| params.release.isValid() || params.release.isValid()
|| params.trackList.isValid() || params.trackList.isValid()
@@ -54,39 +53,27 @@ namespace lms::db
{ {
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id"); query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
if (params.artist.isValid())
{
query.join("track_artist_link t_a_l ON t_a_l.track_id = t_e_i_l.track_id");
query.where("t_a_l.artist_id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
{
std::string clause{ "t_a_l.type IN (" };
for (const auto& type : params.trackArtistLinkTypes)
{
if (clause.back() != '(')
clause += ",";
clause += "?";
query.bind(type);
}
clause += ")";
query.where(clause);
}
}
if (params.track.isValid()) if (params.track.isValid())
query.where("t_e_i_l.track_id = ?").bind(params.track); query.where("t_e_i_l.track_id = ?").bind(params.track);
if (params.release.isValid() if (params.release.isValid()
|| params.discNumber.has_value() || params.medium.isValid()
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc) || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
{ {
assert(!(params.release.isValid() && params.medium.isValid()));
assert((params.sortMethod != TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc || params.release.isValid()));
assert((params.sortMethod != TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc || params.medium.isValid()));
query.join("track t ON t_e_i_l.track_id = t.id"); query.join("track t ON t_e_i_l.track_id = t.id");
if (params.release.isValid()) if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
if (params.discNumber.has_value()) if (params.medium.isValid())
query.where("t.disc_number = ?").bind(params.discNumber.value()); query.where("t.medium_id = ?").bind(params.medium);
if (params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc)
query.join("medium m ON m.id = t.medium_id");
} }
if (params.trackList.isValid()) if (params.trackList.isValid())
@@ -107,7 +94,7 @@ namespace lms::db
query.orderBy("t_e_i.size DESC"); query.orderBy("t_e_i.size DESC");
break; break;
case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc: case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC"); query.orderBy("m.position, t.track_number, t_e_i.size DESC");
break; break;
case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc: case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
query.orderBy("t.track_number, t_e_i.size DESC"); query.orderBy("t.track_number, t_e_i.size DESC");
+1 -1
View File
@@ -57,7 +57,7 @@ namespace lms::db
} }
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.where("t_l.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (params.filters.mediaLibrary.isValid() if (params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid() || params.filters.label.isValid()
+7 -6
View File
@@ -119,12 +119,6 @@ namespace lms::db
int end{}; int end{};
}; };
struct DiscInfo
{
std::size_t position;
std::string name;
};
struct FileInfo struct FileInfo
{ {
Wt::WDateTime lastWrittenTime; Wt::WDateTime lastWrittenTime;
@@ -163,6 +157,12 @@ namespace lms::db
Name, Name,
}; };
enum class MediumSortMethod
{
None,
PositionAsc,
};
enum class ReleaseSortMethod enum class ReleaseSortMethod
{ {
None, None,
@@ -215,6 +215,7 @@ namespace lms::db
DateDescAndRelease, DateDescAndRelease,
Release, // order by disc/track number Release, // order by disc/track number
TrackList, // order by asc order in tracklist TrackList, // order by asc order in tracklist
TrackNumber,
}; };
enum class TrackLyricsSortMethod enum class TrackLyricsSortMethod
@@ -34,6 +34,7 @@
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/DirectoryId.hpp" #include "database/objects/DirectoryId.hpp"
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
namespace lms::db namespace lms::db
@@ -50,8 +51,9 @@ namespace lms::db
{ {
std::optional<Range> range; std::optional<Range> range;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
ArtistId artist; // only directory that involve this artist ArtistId artist; // only directoies that involve this artist
ReleaseId release; // only releases that involve this artist MediumId medium; // only directories that involve this medium
ReleaseId release; // only directories that involve this release
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
DirectoryId parentDirectory; // If set, directories that have this parent DirectoryId parentDirectory; // If set, directories that have this parent
bool withNoTrack{}; // If set, directories that do not contain any track bool withNoTrack{}; // If set, directories that do not contain any track
@@ -74,6 +76,11 @@ namespace lms::db
trackArtistLinkTypes = _trackArtistLinkTypes; trackArtistLinkTypes = _trackArtistLinkTypes;
return *this; return *this;
} }
FindParameters& setMedium(MediumId _medium)
{
medium = _medium;
return *this;
}
FindParameters& setRelease(ReleaseId _release) FindParameters& setRelease(ReleaseId _release)
{ {
release = _release; release = _release;
@@ -107,7 +114,7 @@ namespace lms::db
static pointer find(Session& session, const std::filesystem::path& path); static pointer find(Session& session, const std::filesystem::path& path);
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<Directory::pointer> find(Session& session, const FindParameters& params); static RangeResults<Directory::pointer> find(Session& session, const FindParameters& params);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func);
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId); static RangeResults<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId);
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
@@ -26,6 +26,7 @@
#include <Wt/Dbo/Field.h> #include <Wt/Dbo/Field.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include "core/TaggedType.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/DirectoryId.hpp" #include "database/objects/DirectoryId.hpp"
@@ -43,18 +44,22 @@ namespace lms::db
struct FindParameters struct FindParameters
{ {
using ProcessWildcards = core::TaggedBool<class FindParametersProcessWildcardsTag>;
std::optional<Range> range; std::optional<Range> range;
std::string fileStem; // if set, images with this file stem std::string fileStem; // if set, images with this file stem
DirectoryId directory; // if set, images in this directory ProcessWildcards processWildcardsInFileStem{ false }; // if true, replace '*' by '%' for SQL LIKE
DirectoryId directory; // if set, images in this directory
FindParameters& setRange(std::optional<Range> _range) FindParameters& setRange(std::optional<Range> _range)
{ {
range = _range; range = _range;
return *this; return *this;
} }
FindParameters& setFileStem(std::string_view _fileStem) FindParameters& setFileStem(std::string_view _fileStem, ProcessWildcards processWildcards = ProcessWildcards{ false })
{ {
fileStem = _fileStem; fileStem = _fileStem;
processWildcardsInFileStem = processWildcards;
return *this; return *this;
} }
FindParameters& setDirectory(DirectoryId _directory) FindParameters& setDirectory(DirectoryId _directory)
@@ -0,0 +1,132 @@
/*
* 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 <functional>
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h>
#include "database/IdRange.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtworkId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp"
namespace lms::db
{
class Artwork;
class Release;
class Session;
class Track;
class Medium final : public Object<Medium, MediumId>
{
public:
static const std::size_t maxMediaLength{ 64 };
Medium() = default;
// find
struct FindParameters
{
ReleaseId release;
MediumSortMethod sortMethod{ MediumSortMethod::None };
std::optional<Range> range;
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setSortMethod(MediumSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
};
static std::size_t getCount(Session& session);
static pointer find(Session& session, MediumId id);
static pointer find(Session& session, ReleaseId id, std::optional<std::size_t> position);
static void find(Session& session, const IdRange<MediumId>& idRange, const std::function<void(const Medium::pointer&)>& func);
static IdRange<MediumId> findNextIdRange(Session& session, MediumId lastRetrievedId, std::size_t count);
static RangeResults<MediumId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Updates
static void updatePreferredArtwork(Session& session, MediumId mediumId, ArtworkId artworkId);
// getters
std::string_view getName() const { return _name; }
std::optional<std::size_t> getPosition() const { return _position; }
std::optional<std::size_t> getTrackCount() const { return _trackCount; } // not necessarily the number of tracks in the medium, but the number of tracks that should be in the medium
std::string_view getMedia() const { return _media; }
std::optional<float> getReplayGain() const { return _replayGain; }
ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<Artwork> getPreferredArtwork() const { return _preferredArtwork; }
ArtworkId getPreferredArtworkId() const { return _preferredArtwork.id(); }
// setters
void setName(std::string_view name) { _name = name; }
void setPosition(std::optional<std::size_t> position) { _position = position; }
void setTrackCount(std::optional<std::size_t> trackCount) { _trackCount = trackCount; }
void setMedia(std::string_view media) { _media = media; }
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _position, "position");
Wt::Dbo::field(a, _trackCount, "track_count");
Wt::Dbo::field(a, _media, "media");
Wt::Dbo::field(a, _replayGain, "replay_gain");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "medium");
}
private:
friend class Session;
Medium(ObjectPtr<Release> release);
static pointer create(Session& session, ObjectPtr<Release> release);
std::string _name;
std::optional<int> _position; // position in the release
std::optional<int> _trackCount;
std::string _media; // CD, etc.
std::optional<float> _replayGain;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<Artwork> _preferredArtwork;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // tracks that match this medium
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 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(MediumId)
@@ -51,6 +51,7 @@ namespace lms::db
class Artwork; class Artwork;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Medium;
class Release; class Release;
class Session; class Session;
class Track; class Track;
@@ -282,9 +283,7 @@ namespace lms::db
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 core::UUID::fromString(_MBID); }
std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); } std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } // the number of discs this release should have if complete
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
std::vector<DiscInfo> getDiscs() const;
std::chrono::milliseconds getDuration() const; std::chrono::milliseconds getDuration() const;
Wt::WDateTime getAddedTime() const; Wt::WDateTime getAddedTime() const;
Wt::WDateTime getLastWrittenTime() const; Wt::WDateTime getLastWrittenTime() const;
@@ -301,6 +300,7 @@ namespace lms::db
std::string_view getComment() const { return _comment; } std::string_view getComment() const { return _comment; }
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const; ArtworkId getPreferredArtworkId() const;
std::vector<ObjectPtr<Medium>> getMediums() const;
// Setters // Setters
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
@@ -326,7 +326,6 @@ namespace lms::db
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
bool hasVariousArtists() const; bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const; std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
bool hasDiscSubtitle() const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -43,6 +43,7 @@
#include "database/objects/DirectoryId.hpp" #include "database/objects/DirectoryId.hpp"
#include "database/objects/Filters.hpp" #include "database/objects/Filters.hpp"
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MediumId.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"
@@ -58,6 +59,7 @@ namespace lms::db
class Directory; class Directory;
class TrackEmbeddedImageLink; class TrackEmbeddedImageLink;
class MediaLibrary; class MediaLibrary;
class Medium;
class Release; class Release;
class Session; class Session;
class TrackArtistLink; class TrackArtistLink;
@@ -82,11 +84,11 @@ namespace lms::db
std::string artistName; // only tracks that involve this artist name std::string artistName; // only tracks that involve this artist name
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease{}; // only tracks that do not belong to a release bool nonRelease{}; // only tracks that do not belong to a release
MediumId medium; // matching this medium
ReleaseId release; // matching this release ReleaseId release; // matching this release
std::string releaseName; // matching this release name std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number
DirectoryId directory; // if set, tracks in this directory DirectoryId directory; // if set, tracks in this directory
std::optional<std::size_t> fileSize; // if set, tracks that match this file size std::optional<std::size_t> fileSize; // if set, tracks that match this file size
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
@@ -144,6 +146,11 @@ namespace lms::db
nonRelease = _nonRelease; nonRelease = _nonRelease;
return *this; return *this;
} }
FindParameters& setMedium(MediumId _medium)
{
medium = _medium;
return *this;
}
FindParameters& setRelease(ReleaseId _release) FindParameters& setRelease(ReleaseId _release)
{ {
release = _release; release = _release;
@@ -164,11 +171,6 @@ namespace lms::db
trackNumber = _trackNumber; trackNumber = _trackNumber;
return *this; return *this;
} }
FindParameters& setDiscNumber(int _discNumber)
{
discNumber = _discNumber;
return *this;
}
FindParameters& setDirectory(DirectoryId _directory) FindParameters& setDirectory(DirectoryId _directory)
{ {
directory = _directory; directory = _directory;
@@ -217,9 +219,6 @@ namespace lms::db
// Accessors // Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; } void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(std::optional<int> num) { _trackNumber = num; } void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
void setDiscNumber(std::optional<int> num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
void setDiscSubtitle(std::string_view name) { _discSubtitle = name; }
void setName(std::string_view name); void setName(std::string_view name);
void setAbsoluteFilePath(const std::filesystem::path& filePath); void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
@@ -237,13 +236,13 @@ namespace lms::db
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; }
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; } void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void setComment(std::string_view comment) { _comment = comment; } void setComment(std::string_view comment) { _comment = comment; }
void clearArtistLinks(); void clearArtistLinks();
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink); void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
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 setClusters(const std::vector<ObjectPtr<Cluster>>& clusters); void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
void clearLyrics(); void clearLyrics();
void clearEmbeddedLyrics(); void clearEmbeddedLyrics();
@@ -257,9 +256,6 @@ namespace lms::db
std::size_t getScanVersion() const { return _scanVersion; } std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; } std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::string getName() const { return _name; } std::string getName() const { return _name; }
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; } const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
long long getFileSize() const { return _fileSize; } long long getFileSize() const { return _fileSize; }
@@ -281,8 +277,7 @@ namespace lms::db
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
Advisory getAdvisory() const { return _advisory; } Advisory getAdvisory() const { return _advisory; }
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<float> getReplayGain() const { return _replayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::string_view getComment() const { return _comment; } std::string_view getComment() const { return _comment; }
@@ -292,6 +287,8 @@ namespace lms::db
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const; std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
ReleaseId getReleaseId() const { return _release.id(); } ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
MediumId getMediumId() const { return _medium.id(); }
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;
ObjectPtr<MediaLibrary> getMediaLibrary() const; ObjectPtr<MediaLibrary> getMediaLibrary() const;
@@ -308,9 +305,6 @@ namespace lms::db
{ {
Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _discNumber, "disc_number");
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _bitrate, "bitrate"); Wt::Dbo::field(a, _bitrate, "bitrate");
@@ -328,11 +322,11 @@ namespace lms::db
Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url"); Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _advisory, "advisory"); Wt::Dbo::field(a, _advisory, "advisory");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _replayGain, "replay_gain");
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
Wt::Dbo::belongsTo(a, _medium, "medium", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
@@ -353,10 +347,7 @@ namespace lms::db
static constexpr std::size_t _maxCopyrightURLLength{ 512 }; static constexpr std::size_t _maxCopyrightURLLength{ 512 };
int _scanVersion{}; int _scanVersion{};
std::optional<int> _trackNumber{}; std::optional<int> _trackNumber;
std::optional<int> _discNumber{};
std::optional<int> _totalTrack{};
std::string _discSubtitle;
std::string _name; std::string _name;
int _bitrate{}; // in bps int _bitrate{}; // in bps
int _bitsPerSample{}; int _bitsPerSample{};
@@ -374,10 +365,10 @@ namespace lms::db
std::string _copyright; std::string _copyright;
std::string _copyrightURL; std::string _copyrightURL;
Advisory _advisory{ Advisory::UnSet }; Advisory _advisory{ Advisory::UnSet };
std::optional<float> _trackReplayGain; std::optional<float> _replayGain;
std::optional<float> _releaseReplayGain;
std::string _artistDisplayName; std::string _artistDisplayName;
std::string _comment; std::string _comment;
Wt::Dbo::ptr<Medium> _medium;
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary; Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
Wt::Dbo::ptr<Directory> _directory; Wt::Dbo::ptr<Directory> _directory;
@@ -26,10 +26,9 @@
#include <Wt/Dbo/Field.h> #include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h> #include <Wt/Dbo/collection.h>
#include "core/EnumSet.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/MediumId.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"
@@ -49,11 +48,9 @@ namespace lms::db
struct FindParameters struct FindParameters
{ {
std::optional<Range> range; std::optional<Range> range;
ArtistId artist;
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes;
TrackId track; TrackId track;
ReleaseId release; ReleaseId release;
std::optional<int> discNumber; MediumId medium;
TrackListId trackList; TrackListId trackList;
std::optional<ImageType> imageType; std::optional<ImageType> imageType;
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
@@ -63,13 +60,6 @@ namespace lms::db
range = _range; range = _range;
return *this; return *this;
} }
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
{
artist = _artist;
trackArtistLinkTypes = _trackArtistLinkTypes;
return *this;
}
FindParameters& setTrack(TrackId _track) FindParameters& setTrack(TrackId _track)
{ {
track = _track; track = _track;
@@ -80,9 +70,9 @@ namespace lms::db
release = _release; release = _release;
return *this; return *this;
} }
FindParameters& setDiscNumber(std::optional<int> _discNumber) FindParameters& setMedium(MediumId _medium)
{ {
discNumber = _discNumber; medium = _medium;
return *this; return *this;
} }
FindParameters& setTrackList(TrackListId _trackList) FindParameters& setTrackList(TrackListId _trackList)
+1
View File
@@ -10,6 +10,7 @@ add_executable(test-database
Directory.cpp Directory.cpp
Image.cpp Image.cpp
Listen.cpp Listen.cpp
Medium.cpp
Migration.cpp Migration.cpp
PlayListFile.cpp PlayListFile.cpp
RatedArtist.cpp RatedArtist.cpp
+55
View File
@@ -20,10 +20,12 @@
#include "Common.hpp" #include "Common.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Medium.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedDirectory = ScopedEntity<db::Directory>; using ScopedDirectory = ScopedEntity<db::Directory>;
using ScopedMedium = ScopedEntity<db::Medium>;
TEST_F(DatabaseFixture, Directory) TEST_F(DatabaseFixture, Directory)
{ {
@@ -285,4 +287,57 @@ namespace lms::db::tests
EXPECT_EQ(res.size(), 0); EXPECT_EQ(res.size(), 0);
} }
} }
TEST_F(DatabaseFixture, Directory_findByMedium)
{
ScopedDirectory dir1{ session, "/root" };
ScopedDirectory dir2{ session, "/root" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedRelease release1{ session, "Release1" };
ScopedMedium medium1{ session, release1.lockAndGet() };
ScopedRelease release2{ session, "Release2" };
ScopedMedium medium2{ session, release1.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Directory::FindParameters params;
params.setMedium(medium1.getId());
bool visited{};
Directory::find(session, params, [&](const Directory::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setMedium(medium1.get());
track1.get().modify()->setRelease(release1.get());
track1.get().modify()->setDirectory(dir1.get());
track2.get().modify()->setMedium(medium2.get());
track2.get().modify()->setRelease(release2.get());
track2.get().modify()->setDirectory(dir2.get());
}
{
auto transaction{ session.createReadTransaction() };
Directory::FindParameters params;
params.setMedium(medium1.getId());
std::vector<DirectoryId> visitedDirectories;
Directory::find(session, params, [&](const Directory::pointer& dir) {
visitedDirectories.push_back(dir->getId());
});
ASSERT_EQ(visitedDirectories.size(), 1);
EXPECT_EQ(visitedDirectories[0], dir1.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+98
View File
@@ -122,4 +122,102 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Image_findByFileStem)
{
ScopedDirectory directory{ session, "/path/to" };
ScopedImage image{ session, "/path/to/image" };
const std::string fileStem{ "image" };
{
auto transaction{ session.createWriteTransaction() };
image.get().modify()->setDirectory(directory.get());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem(fileStem);
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem(fileStem, Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("nonexistent");
const auto results{ Image::find(session, params) };
EXPECT_EQ(results.results.size(), 0);
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("ima*");
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 0);
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("ima*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("*ge", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("*g*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("*", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 1);
EXPECT_EQ(results.results[0]->getId(), image.getId());
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("ima%");
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 0);
}
{
auto transaction{ session.createReadTransaction() };
Image::FindParameters params;
params.setFileStem("ima%", Image::FindParameters::ProcessWildcards{ true });
const auto results{ Image::find(session, params) };
ASSERT_EQ(results.results.size(), 0);
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+195
View File
@@ -0,0 +1,195 @@
/*
* Copyright (C) 2024 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"
#include "database/Types.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp"
namespace lms::db::tests
{
using ScopedMedium = ScopedEntity<db::Medium>;
TEST_F(DatabaseFixture, Medium)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Medium::getCount(session), 0);
Medium::pointer medium{ Medium::find(session, MediumId{}) };
ASSERT_EQ(medium, Medium::pointer{});
}
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Medium::getCount(session), 1);
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getReleaseId(), release.getId());
EXPECT_EQ(foundMedium->getPosition(), std::nullopt);
EXPECT_EQ(foundMedium->getMedia(), "");
EXPECT_EQ(foundMedium->getName(), "");
}
{
auto transaction{ session.createWriteTransaction() };
medium.get().modify()->setName("MySubtitle");
medium.get().modify()->setPosition(1);
medium.get().modify()->setMedia("MyMedia");
}
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getName(), "MySubtitle");
EXPECT_EQ(foundMedium->getPosition(), 1);
EXPECT_EQ(foundMedium->getMedia(), "MyMedia");
}
}
TEST_F(DatabaseFixture, MediumFindByRelease_noPosition)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
ASSERT_EQ(foundMedium, Medium::pointer{});
}
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getId(), medium.getId());
}
}
TEST_F(DatabaseFixture, MediumFindByRelease_withPosition)
{
ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
ASSERT_EQ(foundMedium, Medium::pointer{});
}
{
auto transaction{ session.createWriteTransaction() };
medium.get().modify()->setPosition(1);
}
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getId(), medium.getId());
}
}
TEST_F(DatabaseFixture, Medium_findTracksByMedium)
{
ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
bool visited{};
db::Track::find(session, params, [&](const db::Track::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release.get());
track2.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
bool visited{};
db::Track::find(session, params, [&](const db::Track::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setMedium(medium.get());
track2.get().modify()->setMedium(medium.get());
track1.get().modify()->setTrackNumber(3);
track2.get().modify()->setTrackNumber(1);
}
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
std::vector<db::TrackId> visitedTrackIds;
db::Track::find(session, params, [&](const db::Track::pointer& track) {
visitedTrackIds.push_back(track->getId());
});
ASSERT_EQ(visitedTrackIds.size(), 2);
EXPECT_EQ(visitedTrackIds[0], track2.getId());
EXPECT_EQ(visitedTrackIds[1], track1.getId());
}
}
} // namespace lms::db::tests
+2
View File
@@ -25,6 +25,7 @@
#include "database/objects/AuthToken.hpp" #include "database/objects/AuthToken.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/PlayQueue.hpp" #include "database/objects/PlayQueue.hpp"
#include "database/objects/RatedArtist.hpp" #include "database/objects/RatedArtist.hpp"
@@ -353,6 +354,7 @@ VALUES
EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{})); EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{}));
EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{})); EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{}));
EXPECT_FALSE(Image::find(session, ImageId{})); EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Medium::find(session, MediumId{}));
EXPECT_FALSE(Label::find(session, LabelId{})); EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{})); EXPECT_FALSE(Listen::find(session, ListenId{}));
EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{})); EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{}));
+58 -64
View File
@@ -22,13 +22,15 @@
#include "core/PartialDateTime.hpp" #include "core/PartialDateTime.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedArtwork = ScopedEntity<db::Artwork>; using ScopedArtwork = ScopedEntity<db::Artwork>;
using ScopedCountry = ScopedEntity<db::Country>;
using ScopedImage = ScopedEntity<db::Image>; using ScopedImage = ScopedEntity<db::Image>;
using ScopedLabel = ScopedEntity<db::Label>; using ScopedLabel = ScopedEntity<db::Label>;
using ScopedCountry = ScopedEntity<db::Country>; using ScopedMedium = ScopedEntity<db::Medium>;
using ScopedReleaseType = ScopedEntity<db::ReleaseType>; using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
TEST_F(DatabaseFixture, Release) TEST_F(DatabaseFixture, Release)
@@ -438,15 +440,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setTotalTrack(36);
release1.get().modify()->setTotalDisc(6); release1.get().modify()->setTotalDisc(6);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 6); EXPECT_EQ(*release1->getTotalDisc(), 6);
} }
@@ -456,15 +455,12 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setRelease(release1.get()); track2.get().modify()->setRelease(release1.get());
track2.get().modify()->setTotalTrack(37);
release1.get().modify()->setTotalDisc(67); release1.get().modify()->setTotalDisc(67);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 67); EXPECT_EQ(*release1->getTotalDisc(), 67);
} }
@@ -481,18 +477,13 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track3.get().modify()->setRelease(release2.get()); track3.get().modify()->setRelease(release2.get());
track3.get().modify()->setTotalTrack(7);
release2.get().modify()->setTotalDisc(5); release2.get().modify()->setTotalDisc(5);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release2->getTotalDisc(), 5); EXPECT_EQ(*release2->getTotalDisc(), 5);
ASSERT_TRUE(track3->getTotalTrack());
EXPECT_EQ(*track3->getTotalTrack(), 7);
ASSERT_TRUE(release2->getTotalDisc()); ASSERT_TRUE(release2->getTotalDisc());
EXPECT_EQ(*release2->getTotalDisc(), 5); EXPECT_EQ(*release2->getTotalDisc(), 5);
} }
@@ -501,7 +492,11 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
{ {
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedMedium medium1A{ session, release1.lockAndGet() };
ScopedMedium medium1B{ session, release1.lockAndGet() };
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
ScopedMedium medium2A{ session, release2.lockAndGet() };
ScopedMedium medium2B{ session, release2.lockAndGet() };
ScopedTrack track1A{ session }; ScopedTrack track1A{ session };
ScopedTrack track1B{ session }; ScopedTrack track1B{ session };
@@ -519,17 +514,21 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track1A.get().modify()->setRelease(release1.get()); track1A.get().modify()->setRelease(release1.get());
track1A.get().modify()->setMedium(medium1A.get());
track1B.get().modify()->setRelease(release1.get()); track1B.get().modify()->setRelease(release1.get());
track1B.get().modify()->setMedium(medium1B.get());
track2A.get().modify()->setRelease(release2.get()); track2A.get().modify()->setRelease(release2.get());
track2A.get().modify()->setMedium(medium2A.get());
track2B.get().modify()->setRelease(release2.get()); track2B.get().modify()->setRelease(release2.get());
track2B.get().modify()->setMedium(medium2B.get());
track1A.get().modify()->setTrackNumber(1); track1A.get().modify()->setTrackNumber(1);
track1B.get().modify()->setTrackNumber(2); track1B.get().modify()->setTrackNumber(2);
track2A.get().modify()->setDiscNumber(2);
track2A.get().modify()->setTrackNumber(1); track2A.get().modify()->setTrackNumber(1);
track2B.get().modify()->setTrackNumber(2); track2B.get().modify()->setTrackNumber(2);
track2B.get().modify()->setDiscNumber(1);
} }
{ {
@@ -537,14 +536,16 @@ namespace lms::db::tests
{ {
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
EXPECT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results.front(), track1A.getId()); EXPECT_EQ(tracks.results[0], track1A.getId());
EXPECT_EQ(tracks.results[1], track1B.getId());
} }
{ {
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
EXPECT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results.front(), track2B.getId()); EXPECT_EQ(tracks.results[0], track2A.getId());
EXPECT_EQ(tracks.results[1], track2B.getId());
} }
} }
} }
@@ -815,52 +816,6 @@ namespace lms::db::tests
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1);
} }
} }
TEST_F(DatabaseFixture, Release_getDiscCount)
{
ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setDiscNumber(5);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 1);
}
{
auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setRelease(release.get());
track2.get().modify()->setDiscNumber(5);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 1);
}
{
auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setDiscNumber(6);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 2);
}
}
TEST_F(DatabaseFixture, Release_isCompilation) TEST_F(DatabaseFixture, Release_isCompilation)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -1549,4 +1504,43 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Release_mediums)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
EXPECT_EQ(mediums.size(), 0);
}
ScopedMedium medium2{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
ASSERT_EQ(mediums.size(), 1);
EXPECT_EQ(mediums[0]->getId(), medium2.getId());
}
ScopedMedium medium1{ session, release.lockAndGet() };
{
auto transaction{ session.createWriteTransaction() };
medium1.get().modify()->setPosition(1);
medium2.get().modify()->setPosition(2);
}
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
ASSERT_EQ(mediums.size(), 2);
EXPECT_EQ(mediums[0]->getId(), medium1.getId());
EXPECT_EQ(mediums[1]->getId(), medium2.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+36 -48
View File
@@ -20,6 +20,7 @@
#include <limits> #include <limits>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -28,6 +29,7 @@
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedMedium = ScopedEntity<db::Medium>;
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>; using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>; using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>;
@@ -113,6 +115,7 @@ namespace lms::db::tests
ScopedTrackEmbeddedImage image{ session }; ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{ {
@@ -133,6 +136,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
track.get().modify()->setMedium(medium.get());
} }
{ {
@@ -151,13 +155,24 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setTrack(track.getId()); params.setMedium(medium.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited); EXPECT_TRUE(visited);
} }
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setTrack(track.getId());
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
} }
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts) TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts)
@@ -169,6 +184,8 @@ namespace lms::db::tests
ScopedTrack track1{ session }; ScopedTrack track1{ session };
ScopedTrack track2{ session }; ScopedTrack track2{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium1{ session, release.lockAndGet() };
ScopedMedium medium2{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() }; ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() };
ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() }; ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() };
ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() }; ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() };
@@ -176,7 +193,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
medium1.get().modify()->setPosition(1);
medium2.get().modify()->setPosition(2);
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
track1.get().modify()->setMedium(medium1.get());
track1.get().modify()->setTrackNumber(2); track1.get().modify()->setTrackNumber(2);
link1.get().modify()->setType(ImageType::FrontCover); link1.get().modify()->setType(ImageType::FrontCover);
@@ -187,6 +208,7 @@ namespace lms::db::tests
image3.get().modify()->setSize(2000); image3.get().modify()->setSize(2000);
track2.get().modify()->setRelease(release.get()); track2.get().modify()->setRelease(release.get());
track2.get().modify()->setMedium(medium2.get());
track2.get().modify()->setTrackNumber(1); track2.get().modify()->setTrackNumber(1);
link4.get().modify()->setType(ImageType::Media); link4.get().modify()->setType(ImageType::Media);
@@ -213,16 +235,15 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setRelease(release.getId()); params.setMedium(medium1.getId());
params.setImageType(ImageType::Media); params.setImageType(ImageType::Media);
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
std::vector<TrackEmbeddedImageId> visitedIds; std::vector<TrackEmbeddedImageId> visitedIds;
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
ASSERT_EQ(visitedIds.size(), 3); ASSERT_EQ(visitedIds.size(), 2);
EXPECT_EQ(visitedIds[0], image4.getId()); EXPECT_EQ(visitedIds[0], image3.getId());
EXPECT_EQ(visitedIds[1], image3.getId()); EXPECT_EQ(visitedIds[1], image2.getId());
EXPECT_EQ(visitedIds[2], image2.getId());
} }
{ {
@@ -236,9 +257,9 @@ namespace lms::db::tests
std::vector<TrackEmbeddedImageId> visitedIds; std::vector<TrackEmbeddedImageId> visitedIds;
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
ASSERT_EQ(visitedIds.size(), 3); ASSERT_EQ(visitedIds.size(), 3);
EXPECT_EQ(visitedIds[0], image4.getId()); EXPECT_EQ(visitedIds[0], image3.getId());
EXPECT_EQ(visitedIds[1], image3.getId()); EXPECT_EQ(visitedIds[1], image2.getId());
EXPECT_EQ(visitedIds[2], image2.getId()); EXPECT_EQ(visitedIds[2], image4.getId());
} }
{ {
@@ -255,59 +276,26 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist) TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_medium)
{
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setArtist(artist.getId());
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
ASSERT_TRUE(visited);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist });
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
ASSERT_FALSE(visited);
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_discNumber)
{ {
ScopedTrackEmbeddedImage image{ session }; ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
ScopedMedium otherMedium{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
track.get().modify()->setDiscNumber(1); track.get().modify()->setMedium(medium.get());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setDiscNumber(1); params.setMedium(medium.getId());
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
@@ -318,7 +306,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setDiscNumber(2); params.setMedium(2);
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
@@ -41,12 +41,12 @@ namespace lms::image::GraphicsMagick
catch (Magick::Warning& e) catch (Magick::Warning& e)
{ {
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what()); LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
throw Exception{ std::string{ "Magick read warning: " } + e.what() }; throw Exception{ std::string{ "Read warning: " } + e.what() };
} }
catch (Magick::Exception& e) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what()); LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw Exception{ std::string{ "Magick read error: " } + e.what() }; throw Exception{ std::string{ "Read error: " } + e.what() };
} }
} }
@@ -63,12 +63,12 @@ namespace lms::image::GraphicsMagick
catch (Magick::Warning& e) catch (Magick::Warning& e)
{ {
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what()); LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
throw Exception{ std::string{ "Magick read warning: " } + e.what() }; throw Exception{ std::string{ "Read warning: " } + e.what() };
} }
catch (Magick::Exception& e) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what()); LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw Exception{ std::string{ "Magick read error: " } + e.what() }; throw Exception{ std::string{ "Read error: " } + e.what() };
} }
} }
@@ -93,7 +93,7 @@ namespace lms::image::GraphicsMagick
catch (Magick::Exception& e) catch (Magick::Exception& e)
{ {
LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what()); LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what());
throw Exception{ std::string{ "Magick resize error: " } + e.what() }; throw Exception{ std::string{ "Resize error: " } + e.what() };
} }
} }
+9 -6
View File
@@ -23,14 +23,17 @@
namespace lms::image namespace lms::image
{ {
StbiException::StbiException(std::string_view desc) namespace
: Exception{ std::string{ desc } + ": " + getLastFailureReason() }
{ {
} std::string getLastStbiFailureReason()
{
const char* failureReason{ ::stbi_failure_reason() };
return failureReason ? failureReason : "unknown reason";
}
} // namespace
std::string StbiException::getLastFailureReason() StbiException::StbiException(std::string_view desc)
: Exception{ std::string{ desc } + ": " + getLastStbiFailureReason() }
{ {
const char* failureReason{ ::stbi_failure_reason() };
return failureReason ? failureReason : "unknown reason";
} }
} // namespace lms::image } // namespace lms::image
-3
View File
@@ -29,8 +29,5 @@ namespace lms::image
{ {
public: public:
StbiException(std::string_view desc); StbiException(std::string_view desc);
private:
static std::string getLastFailureReason();
}; };
} // namespace lms::image } // namespace lms::image
@@ -46,7 +46,7 @@ namespace lms::auth
void LoginThrottler::removeOutdatedEntries() void LoginThrottler::removeOutdatedEntries()
{ {
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; const Clock::time_point now{ Clock::now() };
for (auto it{ std::begin(_attemptsInfo) }; it != std::end(_attemptsInfo);) for (auto it{ std::begin(_attemptsInfo) }; it != std::end(_attemptsInfo);)
{ {
@@ -60,7 +60,7 @@ namespace lms::auth
void LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address) void LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
{ {
const boost::asio::ip::address clientAddress{ getAddressToThrottle(address) }; const boost::asio::ip::address clientAddress{ getAddressToThrottle(address) };
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() }; const Clock::time_point now{ Clock::now() };
if (_attemptsInfo.size() >= _maxEntries) if (_attemptsInfo.size() >= _maxEntries)
removeOutdatedEntries(); removeOutdatedEntries();
@@ -68,7 +68,7 @@ namespace lms::auth
_attemptsInfo.erase(core::random::pickRandom(_attemptsInfo)); _attemptsInfo.erase(core::random::pickRandom(_attemptsInfo));
AttemptInfo& attemptInfo{ _attemptsInfo[address] }; AttemptInfo& attemptInfo{ _attemptsInfo[address] };
if (attemptInfo.nextAttempt.isValid()) if (attemptInfo.nextAttempt != Clock::time_point{})
{ {
assert(attemptInfo.nextAttempt <= now); // should not be called if throttled assert(attemptInfo.nextAttempt <= now); // should not be called if throttled
attemptInfo = {}; attemptInfo = {};
@@ -79,12 +79,12 @@ namespace lms::auth
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount); LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount) if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
{ {
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'"); LMS_LOG(AUTH, INFO, "Throttling '" << clientAddress.to_string() << "' for " << std::chrono::duration_cast<std::chrono::seconds>(_throttlingDuration).count() << " seconds");
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count()); attemptInfo.nextAttempt = now + _throttlingDuration;
} }
else else
{ {
attemptInfo.nextAttempt = {}; attemptInfo.nextAttempt = Clock::time_point{};
} }
} }
@@ -103,9 +103,9 @@ namespace lms::auth
if (it == _attemptsInfo.end()) if (it == _attemptsInfo.end())
return false; return false;
if (!it->second.nextAttempt.isValid()) if (it->second.nextAttempt == Clock::time_point{})
return false; return false;
return it->second.nextAttempt > Wt::WDateTime::currentDateTime(); return it->second.nextAttempt > Clock::now();
} }
} // namespace lms::auth } // namespace lms::auth
@@ -22,15 +22,15 @@
#include <chrono> #include <chrono>
#include <unordered_map> #include <unordered_map>
#include <Wt/WDateTime.h> #include "core/NetAddress.hpp" // for unordered_map of boost::asio::ip::address
#include "core/NetAddress.hpp"
namespace lms::auth namespace lms::auth
{ {
class LoginThrottler class LoginThrottler
{ {
public: public:
using Clock = std::chrono::steady_clock;
LoginThrottler(std::size_t maxEntries) LoginThrottler(std::size_t maxEntries)
: _maxEntries{ maxEntries } {} : _maxEntries{ maxEntries } {}
@@ -52,7 +52,7 @@ namespace lms::auth
struct AttemptInfo struct AttemptInfo
{ {
Wt::WDateTime nextAttempt; Clock::time_point nextAttempt{};
std::size_t badConsecutiveAttemptCount{}; std::size_t badConsecutiveAttemptCount{};
}; };
std::unordered_map<boost::asio::ip::address, AttemptInfo> _attemptsInfo; std::unordered_map<boost::asio::ip::address, AttemptInfo> _attemptsInfo;
+1
View File
@@ -13,6 +13,7 @@ add_library(lmsscanner STATIC
impl/steps/ScanStepArtistReconciliation.cpp impl/steps/ScanStepArtistReconciliation.cpp
impl/steps/ScanStepAssociateArtistImages.cpp impl/steps/ScanStepAssociateArtistImages.cpp
impl/steps/ScanStepAssociateExternalLyrics.cpp impl/steps/ScanStepAssociateExternalLyrics.cpp
impl/steps/ScanStepAssociateMediumImages.cpp
impl/steps/ScanStepAssociatePlayListTracks.cpp impl/steps/ScanStepAssociatePlayListTracks.cpp
impl/steps/ScanStepAssociateReleaseImages.cpp impl/steps/ScanStepAssociateReleaseImages.cpp
impl/steps/ScanStepAssociateTrackImages.cpp impl/steps/ScanStepAssociateTrackImages.cpp
@@ -40,6 +40,7 @@
#include "steps/ScanStepArtistReconciliation.hpp" #include "steps/ScanStepArtistReconciliation.hpp"
#include "steps/ScanStepAssociateArtistImages.hpp" #include "steps/ScanStepAssociateArtistImages.hpp"
#include "steps/ScanStepAssociateExternalLyrics.hpp" #include "steps/ScanStepAssociateExternalLyrics.hpp"
#include "steps/ScanStepAssociateMediumImages.hpp"
#include "steps/ScanStepAssociatePlayListTracks.hpp" #include "steps/ScanStepAssociatePlayListTracks.hpp"
#include "steps/ScanStepAssociateReleaseImages.hpp" #include "steps/ScanStepAssociateReleaseImages.hpp"
#include "steps/ScanStepAssociateTrackImages.hpp" #include "steps/ScanStepAssociateTrackImages.hpp"
@@ -506,7 +507,8 @@ namespace lms::scanner
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params)); _scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params)); _scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params)); // must come after ScanStepAssociateReleaseImages _scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params)); // must come after ScanStepAssociateReleaseImages
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateTrackImages>(params)); // must come after ScanStepAssociateReleaseImages _scanSteps.emplace_back(std::make_unique<ScanStepAssociateMediumImages>(params)); // must come after ScanStepAssociateReleaseImages
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateTrackImages>(params)); // must come after ScanStepAssociateMediumImages and ScanStepAssociateReleaseImages
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params)); _scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params)); _scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params)); _scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
@@ -32,6 +32,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.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"
@@ -222,6 +223,26 @@ namespace lms::scanner
return release; return release;
} }
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
{
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
if (!dbMedium)
dbMedium = session.create<db::Medium>(release);
if (dbMedium->getPosition() != medium.position)
dbMedium.modify()->setPosition(medium.position);
if (dbMedium->getMedia() != medium.media)
dbMedium.modify()->setMedia(medium.media);
if (dbMedium->getName() != medium.name)
dbMedium.modify()->setName(medium.name);
if (dbMedium->getTrackCount() != medium.trackCount)
dbMedium.modify()->setTrackCount(medium.trackCount);
if (dbMedium->getReplayGain() != medium.replayGain)
dbMedium.modify()->setReplayGain(medium.replayGain);
return dbMedium;
}
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track) std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
{ {
std::vector<db::Cluster::pointer> clusters; std::vector<db::Cluster::pointer> clusters;
@@ -387,8 +408,6 @@ namespace lms::scanner
params.setFileSize(fileSize); params.setFileSize(fileSize);
if (parsedTrack.medium) if (parsedTrack.medium)
{ {
if (parsedTrack.medium->position)
params.setDiscNumber(*parsedTrack.medium->position);
if (parsedTrack.medium->release) if (parsedTrack.medium->release)
params.setReleaseName(parsedTrack.medium->release->name); params.setReleaseName(parsedTrack.medium->release->name);
} }
@@ -507,7 +526,7 @@ namespace lms::scanner
} }
catch (const image::Exception& e) catch (const image::Exception& e)
{ {
addError<EmbeddedImageScanError>(getFilePath(), index); addError<EmbeddedImageScanError>(getFilePath(), index, e.what());
} }
index++; index++;
@@ -691,17 +710,22 @@ namespace lms::scanner
for (const auto& [role, performers] : _parsedTrack->performerArtists) for (const auto& [role, performers] : _parsedTrack->performerArtists)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_parsedTrack->medium && _parsedTrack->medium->release) if (_parsedTrack->medium && _parsedTrack->medium->release)
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory)); {
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
}
else else
{
track.modify()->setRelease({}); track.modify()->setRelease({});
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt); track.modify()->setMedium({});
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt); }
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setTrackNumber(_parsedTrack->position); track.modify()->setTrackNumber(_parsedTrack->position);
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
track.modify()->setDate(_parsedTrack->date); track.modify()->setDate(_parsedTrack->date);
track.modify()->setOriginalDate(_parsedTrack->originalDate); track.modify()->setOriginalDate(_parsedTrack->originalDate);
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
@@ -719,7 +743,7 @@ namespace lms::scanner
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
track.modify()->setTrackReplayGain(_parsedTrack->replayGain); track.modify()->setReplayGain(_parsedTrack->replayGain);
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
track.modify()->clearEmbeddedLyrics(); track.modify()->clearEmbeddedLyrics();
@@ -62,7 +62,7 @@ namespace lms::scanner
catch (const image::Exception& e) catch (const image::Exception& e)
{ {
_parsedImageProperties.reset(); _parsedImageProperties.reset();
addError<ImageFileScanError>(getFilePath()); addError<ImageFileScanError>(getFilePath(), e.what());
} }
} }
@@ -43,7 +43,7 @@ namespace lms::scanner
void ScanErrorLogger::visit(const scanner::EmbeddedImageScanError& error) void ScanErrorLogger::visit(const scanner::EmbeddedImageScanError& error)
{ {
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << error.path << " at index " << error.index); LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << error.path << " at index " << error.index << ": " << error.errorMsg);
} }
void ScanErrorLogger::visit(const scanner::NoAudioTrackFoundError& error) void ScanErrorLogger::visit(const scanner::NoAudioTrackFoundError& error)
@@ -68,7 +68,7 @@ namespace lms::scanner
void ScanErrorLogger::visit(const scanner::ImageFileScanError& error) void ScanErrorLogger::visit(const scanner::ImageFileScanError& error)
{ {
LMS_LOG(DBUPDATER, ERROR, "Failed to read image file " << error.path); LMS_LOG(DBUPDATER, ERROR, "Failed to read image file " << error.path << ": " << error.errorMsg);
} }
void ScanErrorLogger::visit(const scanner::LyricsFileScanError& error) void ScanErrorLogger::visit(const scanner::LyricsFileScanError& error)
@@ -73,7 +73,7 @@ namespace lms::scanner
{ {
db::Image::FindParameters params; db::Image::FindParameters params;
params.setDirectory(directory->getId()); params.setDirectory(directory->getId());
params.setFileStem(fileStem); params.setFileStem(fileStem, db::Image::FindParameters::ProcessWildcards{ true }); // no need to sanitize here, user is responsible for providing sanitized file stems in conf file
db::Image::find(session, params, [&](const db::Image::pointer foundImg) { db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
if (!image) if (!image)
@@ -135,13 +135,11 @@ namespace lms::scanner
{ {
// Expect layout like this: // Expect layout like this:
// ReleaseArtist/Release/Tracks' // ReleaseArtist/Release/Tracks'
// /artist.jpg // /someUserConfiguredArtistFile.jpg
// /someOtherUserConfiguredArtistFile.jpg
// //
// Or: // Or:
// ReleaseArtist/SomeGrouping/Release/Tracks' // ReleaseArtist/SomeGrouping/Release/Tracks'
// /artist.jpg // /someUserConfiguredArtistFile.jpg
// /someOtherUserConfiguredArtistFile.jpg
// //
std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
while (true) while (true)
@@ -0,0 +1,308 @@
/*
* 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 "ScanStepAssociateMediumImages.hpp"
#include <array>
#include <cassert>
#include <deque>
#include <set>
#include <span>
#include "core/IConfig.hpp"
#include "core/IJob.hpp"
#include "core/ILogger.hpp"
#include "core/Path.hpp"
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp"
#include "database/objects/Artwork.hpp"
#include "database/objects/ArtworkId.hpp"
#include "database/objects/Directory.hpp"
#include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/Track.hpp"
#include "JobQueue.hpp"
#include "ScanContext.hpp"
#include "database/objects/TrackEmbeddedImage.hpp"
namespace lms::scanner
{
namespace
{
struct MediumArtworkAssociation
{
db::MediumId mediumId;
db::ArtworkId preferredArtworkId;
};
using MediumArtworkAssociationContainer = std::deque<MediumArtworkAssociation>;
struct SearchMediumArtworkParams
{
std::span<const std::string_view> mediumFileNames;
};
db::Image::pointer findImageInDirectory(db::Session& session, const db::Directory::pointer& directory, std::span<const std::string_view> fileStemsToSearch, db::Image::FindParameters::ProcessWildcards processWildcards)
{
db::Image::pointer image;
for (std::string_view fileStem : fileStemsToSearch)
{
db::Image::FindParameters params;
params.setDirectory(directory->getId());
params.setFileStem(fileStem, processWildcards);
db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
if (!image)
image = foundImg;
});
if (image)
break;
}
return image;
}
db::Image::pointer searchImageInDirectories(db::Session& session, const SearchMediumArtworkParams& searchParams, const db::Medium::pointer& medium)
{
db::Image::pointer image;
std::set<std::filesystem::path> mediumPaths;
db::Directory::FindParameters params;
params.setMedium(medium->getId());
// Expect layout like this:
// Release/Tracks
// /NameOfTheDisc.jpg
// /someOtherUserConfiguredMediumFile.jpg
//
// Or:
// Release/CD X/Tracks'
// /NameOfDisc.jpg.jpg
// /someOtherUserConfiguredMediumFile.jpg
//
// We don't expect mediums to be split across multiple directories, so we can just search for the first directory that matches the medium.
db::Directory::find(session, params, [&](const db::Directory::pointer& directory) {
if (image)
return;
if (const std::string mediumName{ core::pathUtils::sanitizeFileStem(medium->getName()) }; !mediumName.empty())
{
std::string_view mediumNameView{ mediumName };
image = findImageInDirectory(session, directory, std::span{ &mediumNameView, 1 }, db::Image::FindParameters::ProcessWildcards{ false });
}
if (!image)
image = findImageInDirectory(session, directory, searchParams.mediumFileNames, db::Image::FindParameters::ProcessWildcards{ true });
});
return image;
}
db::TrackEmbeddedImage::pointer getArtworkFromTracks(db::Session& session, const db::Medium::pointer& medium)
{
db::TrackEmbeddedImage::pointer image;
db::TrackEmbeddedImage::FindParameters params;
params.setMedium(medium->getId());
params.setImageType(db::ImageType::Media);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& foundImage) {
if (image)
return;
image = foundImage;
});
return image;
}
db::Artwork::pointer computePreferredMediumArtwork(db::Session& session, const SearchMediumArtworkParams& searchParams, const db::Medium::pointer& medium)
{
if (const db::Image::pointer image{ searchImageInDirectories(session, searchParams, medium) })
return db::Artwork::find(session, image->getId());
if (const db::TrackEmbeddedImage::pointer image{ getArtworkFromTracks(session, medium) })
return db::Artwork::find(session, image->getId());
return db::Artwork::pointer{};
}
void updateMediumPreferredArtwork(db::Session& session, const MediumArtworkAssociation& mediumArtworkAssociation)
{
db::Medium::updatePreferredArtwork(session, mediumArtworkAssociation.mediumId, mediumArtworkAssociation.preferredArtworkId);
}
void updateMediumPreferredArtworks(db::Session& session, MediumArtworkAssociationContainer& imageAssociations, bool forceFullBatch)
{
constexpr std::size_t writeBatchSize{ 50 };
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
{
auto transaction{ session.createWriteTransaction() };
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
{
updateMediumPreferredArtwork(session, imageAssociations.front());
imageAssociations.pop_front();
}
}
}
std::vector<std::string> constructMediumFileNames()
{
std::vector<std::string> res;
core::Service<core::IConfig>::get()->visitStrings("medium-image-file-names",
[&res](std::string_view fileName) {
res.emplace_back(fileName);
},
{ "discsubtitle" });
return res;
}
bool fetchNextMediumIdRange(db::Session& session, db::MediumId& lastRetrievedId, db::IdRange<db::MediumId>& idRange)
{
constexpr std::size_t readBatchSize{ 100 };
auto transaction{ session.createReadTransaction() };
idRange = db::Medium::findNextIdRange(session, lastRetrievedId, readBatchSize);
lastRetrievedId = idRange.last;
return idRange.isValid();
}
class ComputeMediumArtworkAssociationsJob : public core::IJob
{
public:
ComputeMediumArtworkAssociationsJob(db::IDb& db, const SearchMediumArtworkParams& searchParams, db::IdRange<db::MediumId> mediumIdRange)
: _db{ db }
, _searchParams{ searchParams }
, _mediumIdRange{ mediumIdRange }
{
}
~ComputeMediumArtworkAssociationsJob() override = default;
ComputeMediumArtworkAssociationsJob(const ComputeMediumArtworkAssociationsJob&) = delete;
ComputeMediumArtworkAssociationsJob& operator=(const ComputeMediumArtworkAssociationsJob&) = delete;
std::span<const MediumArtworkAssociation> getAssociations() const { return _associations; }
std::size_t getProcessedMediumCount() const { return _processedMediumCount; }
private:
core::LiteralString getName() const override { return "Associate Medium Artworks"; }
void run() override
{
auto& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };
db::Medium::find(session, _mediumIdRange, [this, &session](const db::Medium::pointer& medium) {
const db::Artwork::pointer preferredArtwork{ computePreferredMediumArtwork(session, _searchParams, medium) };
if (medium->getPreferredArtwork() != preferredArtwork)
{
_associations.push_back(MediumArtworkAssociation{ medium->getId(), preferredArtwork ? preferredArtwork->getId() : db::ArtworkId{} });
if (preferredArtwork)
LMS_LOG(DBUPDATER, DEBUG, "Updating preferred artwork for medium '" << medium->getName() << "(from '" << medium->getRelease()->getName() << "') with image in " << preferredArtwork->getAbsoluteFilePath());
else
LMS_LOG(DBUPDATER, DEBUG, "Removing preferred artwork from medium '" << medium->getName() << "(from '" << medium->getRelease()->getName() << "')");
}
_processedMediumCount++;
});
}
db::IDb& _db;
const SearchMediumArtworkParams& _searchParams;
db::IdRange<db::MediumId> _mediumIdRange;
std::vector<MediumArtworkAssociation> _associations;
std::size_t _processedMediumCount{};
};
} // namespace
ScanStepAssociateMediumImages::ScanStepAssociateMediumImages(InitParams& initParams)
: ScanStepBase{ initParams }
, _mediumFileNames{ constructMediumFileNames() }
{
}
bool ScanStepAssociateMediumImages::needProcess(const ScanContext& context) const
{
return context.stats.getChangesCount() > 0;
}
void ScanStepAssociateMediumImages::process(ScanContext& context)
{
auto& session{ _db.getTLSSession() };
{
auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = db::Artist::getCount(session);
}
std::vector<std::string_view> mediumFileNames;
mediumFileNames.reserve(_mediumFileNames.size());
for (const std::string& fileName : _mediumFileNames)
mediumFileNames.push_back(fileName);
const SearchMediumArtworkParams searchParams{
.mediumFileNames = mediumFileNames,
};
MediumArtworkAssociationContainer mediumArtworkAssociations;
auto processJobsDone = [&](std::span<std::unique_ptr<core::IJob>> jobs) {
if (_abortScan)
return;
for (const auto& job : jobs)
{
const auto& associationJob{ static_cast<const ComputeMediumArtworkAssociationsJob&>(*job) };
const auto& artistAssociations{ associationJob.getAssociations() };
mediumArtworkAssociations.insert(std::end(mediumArtworkAssociations), std::cbegin(artistAssociations), std::cend(artistAssociations));
context.currentStepStats.processedElems += associationJob.getProcessedMediumCount();
}
updateMediumPreferredArtworks(session, mediumArtworkAssociations, true);
_progressCallback(context.currentStepStats);
};
{
JobQueue queue{ getJobScheduler(), 20, processJobsDone, 1, 0.85F };
db::MediumId lastRetrievedMediumId{};
db::IdRange<db::MediumId> mediumIdRange;
while (fetchNextMediumIdRange(session, lastRetrievedMediumId, mediumIdRange))
queue.push(std::make_unique<ComputeMediumArtworkAssociationsJob>(_db, searchParams, mediumIdRange));
}
// process all remaining associations
updateMediumPreferredArtworks(session, mediumArtworkAssociations, false);
}
} // namespace lms::scanner
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
#include "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepAssociateMediumImages : public ScanStepBase
{
public:
ScanStepAssociateMediumImages(InitParams& initParams);
~ScanStepAssociateMediumImages() override = default;
ScanStepAssociateMediumImages(const ScanStepAssociateMediumImages&) = delete;
ScanStepAssociateMediumImages& operator=(const ScanStepAssociateMediumImages&) = delete;
private:
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
core::LiteralString getStepName() const override { return "Associate medium images"; }
bool needProcess(const ScanContext& context) const override;
void process(ScanContext& context) override;
const std::vector<std::string> _mediumFileNames;
};
} // namespace lms::scanner
@@ -67,7 +67,7 @@ namespace lms::scanner
{ {
db::Image::FindParameters params; db::Image::FindParameters params;
params.setDirectory(directory->getId()); params.setDirectory(directory->getId());
params.setFileStem(fileStem); params.setFileStem(fileStem, db::Image::FindParameters::ProcessWildcards{ true }); // no need to sanitize here, user is responsible for providing sanitized file stems in conf file
db::Image::find(session, params, [&](const db::Image::pointer& image) { db::Image::find(session, params, [&](const db::Image::pointer& image) {
if (!artwork) if (!artwork)
@@ -33,6 +33,7 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -105,24 +106,9 @@ namespace lms::scanner
if (res) if (res)
return res; return res;
// fallback on another track of the same disc // fallback on the medium's preferred artwork
const db::ReleaseId releaseId{ track->getReleaseId() }; if (const auto medium{ track->getMedium() })
if (!releaseId.isValid()) res = medium->getPreferredArtwork();
return res;
if (const auto discNumber{ track->getDiscNumber() })
{
db::TrackEmbeddedImage::FindParameters params;
params.setRelease(releaseId);
params.setDiscNumber(discNumber);
params.setImageType(db::ImageType::Media);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) {
if (!res)
res = db::Artwork::find(session, image->getId());
});
}
return res; return res;
} }
@@ -25,6 +25,7 @@
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -45,6 +46,7 @@ namespace lms::scanner
removeOrphanedClusterTypes(context); removeOrphanedClusterTypes(context);
removeOrphanedArtists(context); removeOrphanedArtists(context);
removeOrphanedReleases(context); removeOrphanedReleases(context);
removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key
removeOrphanedReleaseTypes(context); removeOrphanedReleaseTypes(context);
removeOrphanedLabels(context); removeOrphanedLabels(context);
removeOrphanedCountries(context); removeOrphanedCountries(context);
@@ -70,6 +72,12 @@ namespace lms::scanner
removeOrphanedEntries<db::Artist>(context); removeOrphanedEntries<db::Artist>(context);
} }
void ScanStepRemoveOrphanedDbEntries::removeOrphanedMediums(ScanContext& context)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned mediums...");
removeOrphanedEntries<db::Medium>(context);
}
void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleases(ScanContext& context) void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleases(ScanContext& context)
{ {
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases..."); LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned releases...");
@@ -37,6 +37,7 @@ namespace lms::scanner
void removeOrphanedClusters(ScanContext& context); void removeOrphanedClusters(ScanContext& context);
void removeOrphanedClusterTypes(ScanContext& context); void removeOrphanedClusterTypes(ScanContext& context);
void removeOrphanedArtists(ScanContext& context); void removeOrphanedArtists(ScanContext& context);
void removeOrphanedMediums(ScanContext& context);
void removeOrphanedReleases(ScanContext& context); void removeOrphanedReleases(ScanContext& context);
void removeOrphanedReleaseTypes(ScanContext& context); void removeOrphanedReleaseTypes(ScanContext& context);
void removeOrphanedLabels(ScanContext& context); void removeOrphanedLabels(ScanContext& context);
@@ -95,9 +95,12 @@ namespace lms::scanner
struct EmbeddedImageScanError : public AudioFileScanError struct EmbeddedImageScanError : public AudioFileScanError
{ {
EmbeddedImageScanError(const std::filesystem::path& p, unsigned i) EmbeddedImageScanError(const std::filesystem::path& p, unsigned i, std::string_view e)
: AudioFileScanError{ p } : AudioFileScanError{ p }
, index{ i } {} , index{ i }
, errorMsg{ e }
{
}
void accept(ScanErrorVisitor& visitor) const override void accept(ScanErrorVisitor& visitor) const override
{ {
@@ -105,6 +108,7 @@ namespace lms::scanner
} }
unsigned index; unsigned index;
std::string errorMsg;
}; };
struct NoAudioTrackFoundError : public AudioFileScanError struct NoAudioTrackFoundError : public AudioFileScanError
@@ -149,12 +153,16 @@ namespace lms::scanner
struct ImageFileScanError : public ScanError struct ImageFileScanError : public ScanError
{ {
using ScanError::ScanError; ImageFileScanError(const std::filesystem::path& p, std::string_view e)
: ScanError{ p }
, errorMsg{ e } {}
void accept(ScanErrorVisitor& visitor) const override void accept(ScanErrorVisitor& visitor) const override
{ {
visitor.visit(*this); visitor.visit(*this);
} }
std::string errorMsg;
}; };
struct LyricsFileScanError : public ScanError struct LyricsFileScanError : public ScanError
@@ -21,13 +21,12 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/StreamLogger.hpp"
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
using namespace lms; using namespace lms;
// log to stdout // log to stdout
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout, core::EnumSet<core::logging::Severity>{ core::logging::Severity::FATAL, core::logging::Severity::ERROR }) }; core::Service<core::logging::ILogger> logger{ core::logging::createLogger(core::logging::Severity::ERROR) };
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();
+6 -3
View File
@@ -27,6 +27,7 @@
#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/Medium.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"
@@ -207,10 +208,12 @@ namespace lms::api::subsonic
albumNode.addArrayValue("releaseTypes", releaseType); albumNode.addArrayValue("releaseTypes", releaseType);
albumNode.createEmptyArrayChild("discTitles"); albumNode.createEmptyArrayChild("discTitles");
for (const DiscInfo& discInfo : release->getDiscs()) for (const auto& medium : release->getMediums())
{ {
if (!discInfo.name.empty()) if (medium->getName().empty())
albumNode.addArrayChild("discTitles", createDiscTitle(discInfo)); continue;
albumNode.addArrayChild("discTitles", createDiscTitle(medium));
} }
albumNode.createEmptyArrayChild("recordLabels"); albumNode.createEmptyArrayChild("recordLabels");
@@ -19,14 +19,16 @@
#include "responses/DiscTitle.hpp" #include "responses/DiscTitle.hpp"
#include "database/objects/Medium.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createDiscTitle(const db::DiscInfo& discInfo) Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium)
{ {
Response::Node discTitleNode; Response::Node discTitleNode;
discTitleNode.setAttribute("disc", discInfo.position); discTitleNode.setAttribute("disc", medium->getPosition() ? *medium->getPosition() : 0);
discTitleNode.setAttribute("title", discInfo.name); discTitleNode.setAttribute("title", medium->getName());
return discTitleNode; return discTitleNode;
} }
@@ -19,11 +19,16 @@
#pragma once #pragma once
#include "database/Types.hpp" #include "database/Object.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
namespace lms::db
{
class Medium;
}
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createDiscTitle(const db::DiscInfo& discInfo); Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium);
} }
@@ -19,19 +19,23 @@
#include "responses/ReplayGain.hpp" #include "responses/ReplayGain.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track) Response::Node createReplayGainNode(const db::Track::pointer& track, const db::Medium::pointer& medium)
{ {
Response::Node replayGainNode; Response::Node replayGainNode;
if (const auto trackReplayGain{ track->getTrackReplayGain() }) if (const auto trackReplayGain{ track->getReplayGain() })
replayGainNode.setAttribute("trackGain", *trackReplayGain); replayGainNode.setAttribute("trackGain", *trackReplayGain);
if (const auto releaseReplayGain{ track->getReleaseReplayGain() }) if (medium)
replayGainNode.setAttribute("albumGain", *releaseReplayGain); {
if (const auto releaseReplayGain{ medium->getReplayGain() })
replayGainNode.setAttribute("albumGain", *releaseReplayGain);
}
return replayGainNode; return replayGainNode;
} }
@@ -25,10 +25,11 @@
namespace lms::db namespace lms::db
{ {
class Medium;
class Track; class Track;
} } // namespace lms::db
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track); Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track, const db::ObjectPtr<db::Medium>& medium);
} }
+6 -3
View File
@@ -33,6 +33,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.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"
@@ -78,6 +79,8 @@ namespace lms::api::subsonic
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
const auto medium{ track->getMedium() };
Response::Node trackResponse; Response::Node trackResponse;
if (!id3) if (!id3)
@@ -91,8 +94,8 @@ namespace lms::api::subsonic
trackResponse.setAttribute("title", track->getName()); trackResponse.setAttribute("title", track->getName());
if (track->getTrackNumber()) if (track->getTrackNumber())
trackResponse.setAttribute("track", *track->getTrackNumber()); trackResponse.setAttribute("track", *track->getTrackNumber());
if (track->getDiscNumber()) if (medium && medium->getPosition())
trackResponse.setAttribute("discNumber", *track->getDiscNumber()); trackResponse.setAttribute("discNumber", *medium->getPosition());
if (const auto originalYear{ track->getOriginalYear() }) if (const auto originalYear{ track->getOriginalYear() })
trackResponse.setAttribute("year", *originalYear); trackResponse.setAttribute("year", *originalYear);
else if (const auto year{ track->getYear() }) else if (const auto year{ track->getYear() })
@@ -254,7 +257,7 @@ namespace lms::api::subsonic
}; };
trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory())); trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory()));
trackResponse.addChild("replayGain", createReplayGainNode(track)); trackResponse.addChild("replayGain", createReplayGainNode(track, medium));
return trackResponse; return trackResponse;
} }
+1
View File
@@ -16,6 +16,7 @@ add_executable(lms
ui/Utils.cpp ui/Utils.cpp
ui/admin/debug/Database.cpp ui/admin/debug/Database.cpp
ui/admin/debug/Tracing.cpp ui/admin/debug/Tracing.cpp
ui/admin/About.cpp
ui/admin/DebugToolsView.cpp ui/admin/DebugToolsView.cpp
ui/admin/InitWizardView.cpp ui/admin/InitWizardView.cpp
ui/admin/MediaLibrariesView.cpp ui/admin/MediaLibrariesView.cpp
+84 -33
View File
@@ -20,18 +20,19 @@
#include <thread> #include <thread>
#include <Wt/WApplication.h> #include <Wt/WApplication.h>
#include <Wt/WLogSink.h>
#include <Wt/WServer.h> #include <Wt/WServer.h>
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/xml_parser.hpp>
#include "core/IChildProcessManager.hpp" #include "core/IChildProcessManager.hpp"
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/IOContextRunner.hpp" #include "core/IOContextRunner.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/SystemPaths.hpp" #include "core/SystemPaths.hpp"
#include "core/WtLogger.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/IQueryPlanRecorder.hpp" #include "database/IQueryPlanRecorder.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -64,24 +65,6 @@ namespace lms
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency()); return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
} }
core::logging::Severity getLogMinSeverity()
{
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
if (minSeverity == "debug")
return core::logging::Severity::DEBUG;
else if (minSeverity == "info")
return core::logging::Severity::INFO;
else if (minSeverity == "warning")
return core::logging::Severity::WARNING;
else if (minSeverity == "error")
return core::logging::Severity::ERROR;
else if (minSeverity == "fatal")
return core::logging::Severity::FATAL;
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
}
ui::AuthenticationBackend getUIAuthenticationBackend() ui::AuthenticationBackend getUIAuthenticationBackend()
{ {
const std::string backend{ core::stringUtils::stringToLower(core::Service<core::IConfig>::get()->getString("authentication-backend", "internal")) }; const std::string backend{ core::stringUtils::stringToLower(core::Service<core::IConfig>::get()->getString("authentication-backend", "internal")) };
@@ -109,15 +92,13 @@ namespace lms
throw core::LmsException{ "Invalid config value for 'tracing-level'" }; throw core::LmsException{ "Invalid config value for 'tracing-level'" };
} }
std::vector<std::string> generateWtConfig(std::string execPath, core::logging::Severity minSeverity) std::vector<std::string> generateWtConfig(std::string execPath)
{ {
core::IConfig& config{ *core::Service<core::IConfig>::get() }; core::IConfig& config{ *core::Service<core::IConfig>::get() };
std::vector<std::string> args; std::vector<std::string> args;
const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" }; const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" };
const std::filesystem::path wtLogFilePath{ config.getPath("log-file", "") };
const std::filesystem::path wtAccessLogFilePath{ config.getPath("access-log-file", "") };
const std::filesystem::path wtResourcesPath{ config.getPath("wt-resources", "/usr/share/Wt/resources") }; const std::filesystem::path wtResourcesPath{ config.getPath("wt-resources", "/usr/share/Wt/resources") };
args.push_back(execPath); args.push_back(execPath);
@@ -142,19 +123,12 @@ namespace lms
args.push_back("--http-address=" + std::string{ config.getString("listen-addr", "0.0.0.0") }); args.push_back("--http-address=" + std::string{ config.getString("listen-addr", "0.0.0.0") });
} }
if (!wtAccessLogFilePath.empty())
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
args.push_back("--threads=" + std::to_string(getThreadCount())); args.push_back("--threads=" + std::to_string(getThreadCount()));
// Generate the wt_config.xml file // Generate the wt_config.xml file
boost::property_tree::ptree pt; boost::property_tree::ptree pt;
pt.put("server.application-settings.<xmlattr>.location", "*"); pt.put("server.application-settings.<xmlattr>.location", "*");
pt.put("server.application-settings.log-file", wtLogFilePath.string());
// log-config
pt.put("server.application-settings.log-config", core::logging::WtLogger::computeLogConfig(minSeverity));
// Reverse proxy // Reverse proxy
if (config.getBool("behind-reverse-proxy", false)) if (config.getBool("behind-reverse-proxy", false))
@@ -238,6 +212,82 @@ namespace lms
}); });
}); });
} }
core::logging::Severity getLogMinSeverity()
{
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
if (minSeverity == "debug")
return core::logging::Severity::DEBUG;
else if (minSeverity == "info")
return core::logging::Severity::INFO;
else if (minSeverity == "warning")
return core::logging::Severity::WARNING;
else if (minSeverity == "error")
return core::logging::Severity::ERROR;
else if (minSeverity == "fatal")
return core::logging::Severity::FATAL;
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
}
class LmsLogSink : public Wt::WLogSink
{
public:
LmsLogSink(core::logging::ILogger& logger)
: _logger{ logger }
{
}
private:
void log(const std::string& type, const std::string& scope, const std::string& message) const noexcept override
{
// Some wt code path may go here without testing logging()
if (logging(type, scope))
{
const core::logging::Severity severity{ getSeverity(type, scope) };
_logger.processLog(core::logging::Module::WT, severity, message);
}
}
bool logging(const std::string& type, const std::string& scope) const noexcept override
{
const core::logging::Severity severity{ getSeverity(type, scope) };
return _logger.isSeverityActive(severity);
}
static core::logging::Severity getSeverity(const std::string& type, const std::string& scope)
{
return adjustSeverity(getSeverityFromString(type), scope);
}
static core::logging::Severity adjustSeverity(core::logging::Severity initialSeverity, std::string_view scope)
{
if (initialSeverity == core::logging::Severity::INFO && (scope == "WebRequest" || scope == "wthttp"))
return core::logging::Severity::DEBUG;
return initialSeverity;
}
static core::logging::Severity getSeverityFromString(std::string_view type)
{
if (type == "debug")
return core::logging::Severity::DEBUG;
if (type == "info")
return core::logging::Severity::INFO;
if (type == "warning")
return core::logging::Severity::WARNING;
if (type == "error")
return core::logging::Severity::ERROR;
if (type == "fatal")
return core::logging::Severity::FATAL;
return core::logging::Severity::INFO;
}
core::logging::ILogger& _logger;
};
} // namespace } // namespace
int main(int argc, char* argv[]) int main(int argc, char* argv[])
@@ -275,8 +325,7 @@ namespace lms
close(STDIN_FILENO); close(STDIN_FILENO);
core::Service<core::IConfig> config{ core::createConfig(configFilePath) }; core::Service<core::IConfig> config{ core::createConfig(configFilePath) };
const core::logging::Severity minLogSeverity{ getLogMinSeverity() }; core::Service<core::logging::ILogger> logger{ createLogger(getLogMinSeverity(), config->getPath("log-file", "")) };
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::WtLogger>(minLogSeverity) };
core::Service<core::tracing::ITraceLogger> traceLogger; core::Service<core::tracing::ITraceLogger> traceLogger;
if (const auto level{ getTracingLevel() }) if (const auto level{ getTracingLevel() })
traceLogger.assign(core::tracing::createTraceLogger(level.value(), config->getULong("tracing-buffer-size", core::tracing::MinBufferSizeInMBytes))); traceLogger.assign(core::tracing::createTraceLogger(level.value(), config->getULong("tracing-buffer-size", core::tracing::MinBufferSizeInMBytes)));
@@ -292,7 +341,7 @@ namespace lms
std::filesystem::create_directories(config->getPath("working-dir", "/var/lms") / "cache"); std::filesystem::create_directories(config->getPath("working-dir", "/var/lms") / "cache");
// Construct WT configuration and get the argc/argv back // Construct WT configuration and get the argc/argv back
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0], minLogSeverity) }; const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0]) };
std::vector<const char*> wtArgv(wtServerArgs.size()); std::vector<const char*> wtArgv(wtServerArgs.size());
for (std::size_t i = 0; i < wtServerArgs.size(); ++i) for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
@@ -301,8 +350,9 @@ namespace lms
wtArgv[i] = wtServerArgs[i].c_str(); wtArgv[i] = wtServerArgs[i].c_str();
} }
boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop LmsLogSink lmsLogSink{ *logger };
Wt::WServer server{ argv[0] }; Wt::WServer server{ argv[0] };
server.setCustomLogger(lmsLogSink);
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0])); server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
// As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user // As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user
@@ -314,6 +364,7 @@ namespace lms
LMS_LOG(MAIN, INFO, "Starting init web server..."); LMS_LOG(MAIN, INFO, "Starting init web server...");
server.start(); server.start();
boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop
core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" }; core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" };
core::Service<db::IQueryPlanRecorder> queryPlanRecorder; core::Service<db::IQueryPlanRecorder> queryPlanRecorder;
+6
View File
@@ -52,6 +52,7 @@
#include "NotificationContainer.hpp" #include "NotificationContainer.hpp"
#include "PlayQueue.hpp" #include "PlayQueue.hpp"
#include "SettingsView.hpp" #include "SettingsView.hpp"
#include "admin/About.hpp"
#include "admin/DebugToolsView.hpp" #include "admin/DebugToolsView.hpp"
#include "admin/InitWizardView.hpp" #include "admin/InitWizardView.hpp"
#include "admin/MediaLibrariesView.hpp" #include "admin/MediaLibrariesView.hpp"
@@ -76,6 +77,7 @@ namespace lms::ui
const std::string appRoot{ Wt::WApplication::appRoot() }; const std::string appRoot{ Wt::WApplication::appRoot() };
auto res{ std::make_shared<Wt::WMessageResourceBundle>() }; auto res{ std::make_shared<Wt::WMessageResourceBundle>() };
res->use(appRoot + "admin-about");
res->use(appRoot + "admin-db"); res->use(appRoot + "admin-db");
res->use(appRoot + "admin-debugtools"); res->use(appRoot + "admin-debugtools");
res->use(appRoot + "admin-initwizard"); res->use(appRoot + "admin-initwizard");
@@ -460,6 +462,10 @@ namespace lms::ui
if (LmsApp->getUserType() == db::UserType::ADMIN) if (LmsApp->getUserType() == db::UserType::ADMIN)
{ {
navbar->setCondition("if-is-admin", true); navbar->setCondition("if-is-admin", true);
auto* about{ navbar->bindNew<Wt::WPushButton>("about", Wt::WString::tr("Lms.Admin.menu-about"), Wt::TextFormat::XHTML) };
about->clicked().connect([] {
showAboutModal();
});
navbar->bindNew<Wt::WAnchor>("media-libraries", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/libraries" }, Wt::WString::tr("Lms.Admin.menu-media-libraries")); navbar->bindNew<Wt::WAnchor>("media-libraries", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/libraries" }, Wt::WString::tr("Lms.Admin.menu-media-libraries"));
navbar->bindNew<Wt::WAnchor>("scan-settings", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scan-settings" }, Wt::WString::tr("Lms.Admin.menu-scan-settings")); navbar->bindNew<Wt::WAnchor>("scan-settings", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scan-settings" }, Wt::WString::tr("Lms.Admin.menu-scan-settings"));
navbar->bindNew<Wt::WAnchor>("scanner", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scanner" }, Wt::WString::tr("Lms.Admin.menu-scanner")); navbar->bindNew<Wt::WAnchor>("scanner", Wt::WLink{ Wt::LinkType::InternalPath, "/admin/scanner" }, Wt::WString::tr("Lms.Admin.menu-scanner"));
+17 -8
View File
@@ -34,6 +34,7 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -621,14 +622,20 @@ namespace lms::ui
return std::nullopt; return std::nullopt;
case MediaPlayer::Settings::ReplayGain::Mode::Track: case MediaPlayer::Settings::ReplayGain::Mode::Track:
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
break; break;
case MediaPlayer::Settings::ReplayGain::Mode::Release: case MediaPlayer::Settings::ReplayGain::Mode::Release:
gain = track->getReleaseReplayGain(); {
if (!gain) const auto medium{ track->getMedium() };
gain = track->getTrackReplayGain(); if (medium && medium->getReplayGain())
break; gain = medium->getReplayGain();
if (!gain)
gain = track->getReplayGain();
break;
}
case MediaPlayer::Settings::ReplayGain::Mode::Auto: case MediaPlayer::Settings::ReplayGain::Mode::Auto:
{ {
@@ -641,13 +648,15 @@ namespace lms::ui
if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease()) if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease())
|| (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease())) || (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease()))
{ {
gain = track->getReleaseReplayGain(); const auto medium{ track->getMedium() };
if (medium && medium->getReplayGain())
gain = medium->getReplayGain();
if (!gain) if (!gain)
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
} }
else else
{ {
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
} }
break; break;
} }
+26 -28
View File
@@ -87,8 +87,6 @@ namespace lms::ui
}; };
} // namespace } // namespace
using namespace db;
class SettingsModel : public Wt::WFormModel class SettingsModel : public Wt::WFormModel
{ {
public: public:
@@ -192,7 +190,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
User::pointer user{ LmsApp->getUser() }; db::User::pointer user{ LmsApp->getUser() };
{ {
const auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) }; const auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) };
@@ -307,7 +305,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ LmsApp->getUser() }; const db::User::pointer user{ LmsApp->getUser() };
// UI // UI
{ {
@@ -396,7 +394,7 @@ namespace lms::ui
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() })); setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
{ {
const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz }; const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz };
setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz);
validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz);
} }
@@ -472,17 +470,17 @@ namespace lms::ui
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never);
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.if-format-not-supported"), MediaPlayer::Settings::Transcoding::Mode::IfFormatNotSupported);
_transcodingOutputBitrateModel = std::make_shared<ValueStringModel<Bitrate>>(); _transcodingOutputBitrateModel = std::make_shared<ValueStringModel<db::Bitrate>>();
visitAllowedAudioBitrates([&](const Bitrate bitrate) { db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) {
_transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); _transcodingOutputBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate);
}); });
_transcodingOutputFormatModel = std::make_shared<ValueStringModel<TranscodingOutputFormat>>(); _transcodingOutputFormatModel = std::make_shared<ValueStringModel<db::TranscodingOutputFormat>>();
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), TranscodingOutputFormat::MP3); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.mp3"), db::TranscodingOutputFormat::MP3);
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), TranscodingOutputFormat::OGG_OPUS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_opus"), db::TranscodingOutputFormat::OGG_OPUS);
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), TranscodingOutputFormat::MATROSKA_OPUS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.matroska_opus"), db::TranscodingOutputFormat::MATROSKA_OPUS);
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), TranscodingOutputFormat::OGG_VORBIS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.ogg_vorbis"), db::TranscodingOutputFormat::OGG_VORBIS);
_transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), TranscodingOutputFormat::WEBM_VORBIS); _transcodingOutputFormatModel->add(Wt::WString::tr("Lms.Settings.transcoding-output-format.webm_vorbis"), db::TranscodingOutputFormat::WEBM_VORBIS);
_replayGainModeModel = std::make_shared<ReplayGainModeModel>(); _replayGainModeModel = std::make_shared<ReplayGainModeModel>();
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None);
@@ -490,18 +488,18 @@ namespace lms::ui
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track);
_replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release);
_subsonicArtistListModeModel = std::make_shared<ValueStringModel<SubsonicArtistListMode>>(); _subsonicArtistListModeModel = std::make_shared<ValueStringModel<db::SubsonicArtistListMode>>();
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), SubsonicArtistListMode::AllArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), db::SubsonicArtistListMode::AllArtists);
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), SubsonicArtistListMode::ReleaseArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), db::SubsonicArtistListMode::ReleaseArtists);
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), SubsonicArtistListMode::TrackArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), db::SubsonicArtistListMode::TrackArtists);
_feedbackBackendModel = std::make_shared<ValueStringModel<FeedbackBackend>>(); _feedbackBackendModel = std::make_shared<ValueStringModel<db::FeedbackBackend>>();
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), FeedbackBackend::Internal); _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::FeedbackBackend::Internal);
_feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), FeedbackBackend::ListenBrainz); _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::FeedbackBackend::ListenBrainz);
_scrobblingBackendModel = std::make_shared<ValueStringModel<ScrobblingBackend>>(); _scrobblingBackendModel = std::make_shared<ValueStringModel<db::ScrobblingBackend>>();
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), ScrobblingBackend::Internal); _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), db::ScrobblingBackend::Internal);
_scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), ScrobblingBackend::ListenBrainz); _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), db::ScrobblingBackend::ListenBrainz);
} }
auth::IPasswordService* _authPasswordService{}; auth::IPasswordService* _authPasswordService{};
@@ -512,10 +510,10 @@ namespace lms::ui
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel; std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
std::shared_ptr<ArtistRelationshipsModel> _artistRelationshipsModel; std::shared_ptr<ArtistRelationshipsModel> _artistRelationshipsModel;
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel; std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel; std::shared_ptr<ValueStringModel<db::Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel; std::shared_ptr<ValueStringModel<db::TranscodingOutputFormat>> _transcodingOutputFormatModel;
std::shared_ptr<ReplayGainModeModel> _replayGainModeModel; std::shared_ptr<ReplayGainModeModel> _replayGainModeModel;
std::shared_ptr<ValueStringModel<SubsonicArtistListMode>> _subsonicArtistListModeModel; std::shared_ptr<ValueStringModel<db::SubsonicArtistListMode>> _subsonicArtistListModeModel;
std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel; std::shared_ptr<FeedbackBackendModel> _feedbackBackendModel;
std::shared_ptr<ScrobblingBackendModel> _scrobblingBackendModel; std::shared_ptr<ScrobblingBackendModel> _scrobblingBackendModel;
}; };
@@ -740,8 +738,8 @@ namespace lms::ui
} }
auto updateListenBrainzTokenField{ [=] { auto updateListenBrainzTokenField{ [=] {
const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == FeedbackBackend::ListenBrainz const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == db::FeedbackBackend::ListenBrainz
|| model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == ScrobblingBackend::ListenBrainz }; || model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == db::ScrobblingBackend::ListenBrainz };
model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable); model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable);
model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable); model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable);
+47
View File
@@ -0,0 +1,47 @@
/*
* 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 "ModalManager.hpp"
#include <Wt/WPushButton.h>
#include <Wt/WTemplate.h>
#include "core/Version.hpp"
#include "LmsApplication.hpp"
namespace lms::ui
{
void showAboutModal()
{
auto aboutModal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.About.template")) };
Wt::WWidget* aboutModalPtr{ aboutModal.get() };
aboutModal->addFunction("tr", &Wt::WTemplate::Functions::tr);
aboutModal->bindString("version", std::string{ core::getVersion() }, Wt::TextFormat::Plain);
aboutModal->bindString("homepage-link", "https://github.com/epoupon/lms");
Wt::WPushButton* okBtn{ aboutModal->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok")) };
okBtn->clicked().connect([=] {
LmsApp->getModalManager().dispose(aboutModalPtr);
});
LmsApp->getModalManager().show(std::move(aboutModal));
}
} // namespace lms::ui
+25
View File
@@ -0,0 +1,25 @@
/*
* 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
namespace lms::ui
{
void showAboutModal();
} // namespace lms::ui
+1 -3
View File
@@ -83,8 +83,6 @@ namespace lms::ui
void MediaLibrariesView::showDeleteLibraryModal(db::MediaLibraryId mediaLibraryId, Wt::WTemplate* libraryEntry) void MediaLibrariesView::showDeleteLibraryModal(db::MediaLibraryId mediaLibraryId, Wt::WTemplate* libraryEntry)
{ {
using namespace db;
auto modal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.MediaLibraries.template.delete-library")) }; auto modal{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.MediaLibraries.template.delete-library")) };
modal->addFunction("tr", &Wt::WTemplate::Functions::tr); modal->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WWidget* modalPtr{ modal.get() }; Wt::WWidget* modalPtr{ modal.get() };
@@ -94,7 +92,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
db::MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) }; db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) };
if (mediaLibrary) if (mediaLibrary)
mediaLibrary.remove(); mediaLibrary.remove();
} }
+12 -14
View File
@@ -33,14 +33,12 @@
namespace lms::ui namespace lms::ui
{ {
using namespace db;
namespace namespace
{ {
class LibraryNameValidator : public Wt::WValidator class LibraryNameValidator : public Wt::WValidator
{ {
public: public:
LibraryNameValidator(MediaLibraryId libraryId) LibraryNameValidator(db::MediaLibraryId libraryId)
: _libraryId{ libraryId } {} : _libraryId{ libraryId } {}
private: private:
@@ -66,13 +64,13 @@ namespace lms::ui
return result; return result;
} }
const MediaLibraryId _libraryId; const db::MediaLibraryId _libraryId;
}; };
class LibraryRootPathValidator : public Wt::WValidator class LibraryRootPathValidator : public Wt::WValidator
{ {
public: public:
LibraryRootPathValidator(MediaLibraryId libraryId) LibraryRootPathValidator(db::MediaLibraryId libraryId)
: _libraryId{ libraryId } {} : _libraryId{ libraryId } {}
private: private:
@@ -115,7 +113,7 @@ namespace lms::ui
return result; return result;
} }
const MediaLibraryId _libraryId; const db::MediaLibraryId _libraryId;
}; };
class MediaLibraryModel : public Wt::WFormModel class MediaLibraryModel : public Wt::WFormModel
@@ -124,7 +122,7 @@ namespace lms::ui
static inline constexpr Field NameField{ "name" }; static inline constexpr Field NameField{ "name" };
static inline constexpr Field DirectoryField{ "directory" }; static inline constexpr Field DirectoryField{ "directory" };
MediaLibraryModel(MediaLibraryId libraryId) MediaLibraryModel(db::MediaLibraryId libraryId)
: _libraryId{ libraryId } : _libraryId{ libraryId }
{ {
addField(NameField); addField(NameField);
@@ -146,7 +144,7 @@ namespace lms::ui
loadData(); loadData();
} }
MediaLibraryId saveData() db::MediaLibraryId saveData()
{ {
auto& session{ LmsApp->getDbSession() }; auto& session{ LmsApp->getDbSession() };
auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
@@ -154,10 +152,10 @@ namespace lms::ui
std::string name{ valueText(NameField).toUTF8() }; std::string name{ valueText(NameField).toUTF8() };
std::string path{ valueText(DirectoryField).toUTF8() }; std::string path{ valueText(DirectoryField).toUTF8() };
MediaLibrary::pointer library; db::MediaLibrary::pointer library;
if (_libraryId.isValid()) if (_libraryId.isValid())
{ {
library = MediaLibrary::find(session, _libraryId); library = db::MediaLibrary::find(session, _libraryId);
if (library) if (library)
{ {
library.modify()->setName(name); library.modify()->setName(name);
@@ -166,7 +164,7 @@ namespace lms::ui
} }
else else
{ {
library = session.create<MediaLibrary>(name, path); library = session.create<db::MediaLibrary>(name, path);
} }
return library->getId(); return library->getId();
@@ -178,17 +176,17 @@ namespace lms::ui
auto& session{ LmsApp->getDbSession() }; auto& session{ LmsApp->getDbSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const MediaLibrary::pointer library{ MediaLibrary::find(session, _libraryId) }; const db::MediaLibrary::pointer library{ db::MediaLibrary::find(session, _libraryId) };
setValue(NameField, std::string{ library->getName() }); setValue(NameField, std::string{ library->getName() });
setValue(DirectoryField, library->getPath().string()); setValue(DirectoryField, library->getPath().string());
} }
const MediaLibraryId _libraryId; const db::MediaLibraryId _libraryId;
}; };
} // namespace } // namespace
MediaLibraryModal::MediaLibraryModal(MediaLibraryId mediaLibraryId) MediaLibraryModal::MediaLibraryModal(db::MediaLibraryId mediaLibraryId)
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.MediaLibrary.template") } : Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.MediaLibrary.template") }
{ {
auto model{ std::make_shared<MediaLibraryModel>(mediaLibraryId) }; auto model{ std::make_shared<MediaLibraryModel>(mediaLibraryId) };
+1 -1
View File
@@ -29,7 +29,7 @@ namespace lms::ui
class MediaLibraryModal : public Wt::WTemplateFormView class MediaLibraryModal : public Wt::WTemplateFormView
{ {
public: public:
MediaLibraryModal(db::MediaLibraryId mediaLibaryId); MediaLibraryModal(db::MediaLibraryId mediaLibraryId);
Wt::Signal<db::MediaLibraryId>& saved() { return _saved; }; Wt::Signal<db::MediaLibraryId>& saved() { return _saved; };
Wt::Signal<>& cancelled() { return _cancelled; } Wt::Signal<>& cancelled() { return _cancelled; }
+17 -19
View File
@@ -42,8 +42,6 @@
namespace lms::ui namespace lms::ui
{ {
using namespace db;
namespace namespace
{ {
class TagDelimitersValidator : public Wt::WValidator class TagDelimitersValidator : public Wt::WValidator
@@ -75,7 +73,7 @@ namespace lms::ui
static inline constexpr Field ArtistImageFallbackToReleaseField{ "artist-image-fallback-to-release" }; static inline constexpr Field ArtistImageFallbackToReleaseField{ "artist-image-fallback-to-release" };
static inline constexpr Field ArtistsToNotSplitField{ "artists-to-not-split" }; static inline constexpr Field ArtistsToNotSplitField{ "artists-to-not-split" };
using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>; using UpdatePeriodModel = ValueStringModel<db::ScanSettings::UpdatePeriod>;
DatabaseSettingsModel() DatabaseSettingsModel()
{ {
@@ -105,7 +103,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const ScanSettings::pointer scanSettings{ ScanSettings::find(LmsApp->getDbSession()) }; const db::ScanSettings::pointer scanSettings{ db::ScanSettings::find(LmsApp->getDbSession()) };
auto periodRow{ _updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod()) }; auto periodRow{ _updatePeriodModel->getRowFromValue(scanSettings->getUpdatePeriod()) };
if (periodRow) if (periodRow)
@@ -115,8 +113,8 @@ namespace lms::ui
if (startTimeRow) if (startTimeRow)
setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow)); setValue(UpdateStartTimeField, _updateStartTimeModel->getString(*startTimeRow));
if (scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Hourly if (scanSettings->getUpdatePeriod() == db::ScanSettings::UpdatePeriod::Hourly
|| scanSettings->getUpdatePeriod() == ScanSettings::UpdatePeriod::Never) || scanSettings->getUpdatePeriod() == db::ScanSettings::UpdatePeriod::Never)
{ {
setReadOnly(UpdateStartTimeField, true); setReadOnly(UpdateStartTimeField, true);
} }
@@ -147,7 +145,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
ScanSettings::pointer scanSettings{ ScanSettings::find(LmsApp->getDbSession()) }; db::ScanSettings::pointer scanSettings{ db::ScanSettings::find(LmsApp->getDbSession()) };
{ {
const auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) }; const auto updatePeriodRow{ _updatePeriodModel->getRowFromString(valueText(UpdatePeriodField)) };
@@ -197,12 +195,12 @@ namespace lms::ui
void void
initializeModels() initializeModels()
{ {
_updatePeriodModel = std::make_shared<ValueStringModel<ScanSettings::UpdatePeriod>>(); _updatePeriodModel = std::make_shared<ValueStringModel<db::ScanSettings::UpdatePeriod>>();
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), ScanSettings::UpdatePeriod::Never); _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.never"), db::ScanSettings::UpdatePeriod::Never);
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), ScanSettings::UpdatePeriod::Hourly); _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.hourly"), db::ScanSettings::UpdatePeriod::Hourly);
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), ScanSettings::UpdatePeriod::Daily); _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.daily"), db::ScanSettings::UpdatePeriod::Daily);
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), ScanSettings::UpdatePeriod::Weekly); _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.weekly"), db::ScanSettings::UpdatePeriod::Weekly);
_updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), ScanSettings::UpdatePeriod::Monthly); _updatePeriodModel->add(Wt::WString::tr("Lms.Admin.Database.monthly"), db::ScanSettings::UpdatePeriod::Monthly);
_updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>(); _updateStartTimeModel = std::make_shared<ValueStringModel<Wt::WTime>>();
for (std::size_t i = 0; i < 24; ++i) for (std::size_t i = 0; i < 24; ++i)
@@ -211,14 +209,14 @@ namespace lms::ui
_updateStartTimeModel->add(time.toString(), time); _updateStartTimeModel->add(time.toString(), time);
} }
_similarityEngineTypeModel = std::make_shared<ValueStringModel<ScanSettings::SimilarityEngineType>>(); _similarityEngineTypeModel = std::make_shared<ValueStringModel<db::ScanSettings::SimilarityEngineType>>();
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), ScanSettings::SimilarityEngineType::Clusters); _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.clusters"), db::ScanSettings::SimilarityEngineType::Clusters);
_similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), ScanSettings::SimilarityEngineType::None); _similarityEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.similarity-engine-type.none"), db::ScanSettings::SimilarityEngineType::None);
} }
std::shared_ptr<UpdatePeriodModel> _updatePeriodModel; std::shared_ptr<UpdatePeriodModel> _updatePeriodModel;
std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel; std::shared_ptr<ValueStringModel<Wt::WTime>> _updateStartTimeModel;
std::shared_ptr<ValueStringModel<ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel; std::shared_ptr<ValueStringModel<db::ScanSettings::SimilarityEngineType>> _similarityEngineTypeModel;
}; };
class LineEditEntryModel : public Wt::WFormModel class LineEditEntryModel : public Wt::WFormModel
@@ -362,8 +360,8 @@ namespace lms::ui
auto updatePeriod{ std::make_unique<Wt::WComboBox>() }; auto updatePeriod{ std::make_unique<Wt::WComboBox>() };
updatePeriod->setModel(model->updatePeriodModel()); updatePeriod->setModel(model->updatePeriodModel());
updatePeriod->activated().connect([=](int row) { updatePeriod->activated().connect([=](int row) {
const ScanSettings::UpdatePeriod period{ model->updatePeriodModel()->getValue(row) }; const db::ScanSettings::UpdatePeriod period{ model->updatePeriodModel()->getValue(row) };
model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == ScanSettings::UpdatePeriod::Hourly || period == ScanSettings::UpdatePeriod::Never); model->setReadOnly(DatabaseSettingsModel::UpdateStartTimeField, period == db::ScanSettings::UpdatePeriod::Hourly || period == db::ScanSettings::UpdatePeriod::Never);
t->updateModel(model.get()); t->updateModel(model.get());
t->updateView(model.get()); t->updateView(model.get());
}); });
+2 -2
View File
@@ -55,7 +55,7 @@ namespace lms::ui
} }
void visit(const scanner::EmbeddedImageScanError& error) override void visit(const scanner::EmbeddedImageScanError& error) override
{ {
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-embedded-image").arg(error.index).toUTF8() << '\n'; _os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.bad-embedded-image").arg(error.index).arg(Wt::WString::fromUTF8(error.errorMsg)).toUTF8() << '\n';
} }
void visit(const scanner::NoAudioTrackFoundError& error) override void visit(const scanner::NoAudioTrackFoundError& error) override
{ {
@@ -75,7 +75,7 @@ namespace lms::ui
} }
void visit(const scanner::ImageFileScanError& error) override void visit(const scanner::ImageFileScanError& error) override
{ {
_os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file").toUTF8() << '\n'; _os << error.path << ": " << Wt::WString::tr("Lms.Admin.ScannerController.cannot-read-image-file").arg(Wt::WString::fromUTF8(error.errorMsg)).toUTF8() << '\n';
} }
void visit(const scanner::LyricsFileScanError& error) override void visit(const scanner::LyricsFileScanError& error) override
{ {
+15 -17
View File
@@ -42,8 +42,6 @@
namespace lms::ui namespace lms::ui
{ {
using namespace db;
class UserModel : public Wt::WFormModel class UserModel : public Wt::WFormModel
{ {
public: public:
@@ -51,7 +49,7 @@ namespace lms::ui
static inline const Field PasswordField{ "password" }; static inline const Field PasswordField{ "password" };
static inline const Field DemoField{ "demo" }; static inline const Field DemoField{ "demo" };
UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService) UserModel(std::optional<db::UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService)
: _userId{ userId } : _userId{ userId }
, _authPasswordService{ authPasswordService } , _authPasswordService{ authPasswordService }
, _authTokenService{ authTokenService } , _authTokenService{ authTokenService }
@@ -81,7 +79,7 @@ namespace lms::ui
if (_userId) if (_userId)
{ {
// Update user // Update user
User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) }; db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
if (!user) if (!user)
throw UserNotFoundException{}; throw UserNotFoundException{};
@@ -94,16 +92,16 @@ namespace lms::ui
else else
{ {
// Check races with other endpoints (subsonic API...) // Check races with other endpoints (subsonic API...)
User::pointer user{ User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) }; db::User::pointer user{ db::User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
if (user) if (user)
throw UserNotAllowedException{}; throw UserNotAllowedException{};
// Create user // Create user
user = LmsApp->getDbSession().create<User>(valueText(LoginField).toUTF8()); user = LmsApp->getDbSession().create<db::User>(valueText(LoginField).toUTF8());
if (Wt::asNumber(value(DemoField))) if (Wt::asNumber(value(DemoField)))
{ {
user.modify()->setType(UserType::DEMO); user.modify()->setType(db::UserType::DEMO);
// For demo user, we create the subsonic API auth token now as we have no other mean to create it later // For demo user, we create the subsonic API auth token now as we have no other mean to create it later
core::Service<auth::IAuthTokenService>::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString()); core::Service<auth::IAuthTokenService>::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString());
@@ -122,24 +120,24 @@ namespace lms::ui
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) }; const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
if (!user) if (!user)
throw UserNotFoundException{}; throw UserNotFoundException{};
if (user == LmsApp->getUser()) if (user == LmsApp->getUser())
throw UserNotAllowedException{}; throw UserNotAllowedException{};
} }
UserType getUserType() const db::UserType getUserType() const
{ {
if (_userId) if (_userId)
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) }; const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
return user->getType(); return user->getType();
} }
return Wt::asNumber(value(DemoField)) ? UserType::DEMO : UserType::REGULAR; return Wt::asNumber(value(DemoField)) ? db::UserType::DEMO : db::UserType::REGULAR;
} }
std::string getLoginName() const std::string getLoginName() const
@@ -148,7 +146,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ User::find(LmsApp->getDbSession(), *_userId) }; const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *_userId) };
return user->getLoginName(); return user->getLoginName();
} }
@@ -163,7 +161,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) }; const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8()) };
if (user) if (user)
error = Wt::WString::tr("Lms.Admin.User.user-already-exists"); error = Wt::WString::tr("Lms.Admin.User.user-already-exists");
} }
@@ -171,7 +169,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
if (Wt::asNumber(value(DemoField)) && User::findDemoUser(LmsApp->getDbSession())) if (Wt::asNumber(value(DemoField)) && db::User::findDemoUser(LmsApp->getDbSession()))
error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists"); error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists");
} }
@@ -183,7 +181,7 @@ namespace lms::ui
return false; return false;
} }
std::optional<UserId> _userId; std::optional<db::UserId> _userId;
auth::IPasswordService* _authPasswordService{}; auth::IPasswordService* _authPasswordService{};
auth::IAuthTokenService& _authTokenService; auth::IAuthTokenService& _authTokenService;
}; };
@@ -202,7 +200,7 @@ namespace lms::ui
if (!wApp->internalPathMatches("/admin/user")) if (!wApp->internalPathMatches("/admin/user"))
return; return;
const std::optional<UserId> userId{ core::stringUtils::readAs<UserId::ValueType>(wApp->internalPathNextPart("/admin/user/")) }; const std::optional<db::UserId> userId{ core::stringUtils::readAs<db::UserId::ValueType>(wApp->internalPathNextPart("/admin/user/")) };
clear(); clear();
@@ -220,7 +218,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const User::pointer user{ User::find(LmsApp->getDbSession(), *userId) }; const db::User::pointer user{ db::User::find(LmsApp->getDbSession(), *userId) };
if (!user) if (!user)
throw UserNotFoundException{}; throw UserNotFoundException{};
+4 -9
View File
@@ -23,19 +23,14 @@
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/auth/IPasswordService.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "ModalManager.hpp" #include "ModalManager.hpp"
namespace lms::ui namespace lms::ui
{ {
using namespace db;
UsersView::UsersView() UsersView::UsersView()
: Wt::WTemplate{ Wt::WString::tr("Lms.Admin.Users.template") } : Wt::WTemplate{ Wt::WString::tr("Lms.Admin.Users.template") }
{ {
@@ -69,9 +64,9 @@ namespace lms::ui
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const UserId currentUserId{ LmsApp->getUserId() }; const db::UserId currentUserId{ LmsApp->getUserId() };
User::find(LmsApp->getDbSession(), User::FindParameters{}, [&](const User::pointer& user) { db::User::find(LmsApp->getDbSession(), db::User::FindParameters{}, [&](const db::User::pointer& user) {
const UserId userId{ user->getId() }; const db::UserId userId{ user->getId() };
Wt::WTemplate* entry{ _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry")) }; Wt::WTemplate* entry{ _container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Admin.Users.template.entry")) };
@@ -107,7 +102,7 @@ namespace lms::ui
{ {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
User::pointer user{ User::find(LmsApp->getDbSession(), userId) }; db::User::pointer user{ db::User::find(LmsApp->getDbSession(), userId) };
if (user) if (user)
user.remove(); user.remove();
} }
+17 -19
View File
@@ -32,16 +32,14 @@
namespace lms::ui namespace lms::ui
{ {
using namespace db; db::RangeResults<db::ArtistId> ArtistCollector::get(std::optional<db::Range> requestedRange)
RangeResults<ArtistId> ArtistCollector::get(std::optional<db::Range> requestedRange)
{ {
feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() }; feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() };
scrobbling::IScrobblingService& scrobblingService{ *core::Service<scrobbling::IScrobblingService>::get() }; scrobbling::IScrobblingService& scrobblingService{ *core::Service<scrobbling::IScrobblingService>::get() };
const Range range{ getActualRange(requestedRange) }; const Range range{ getActualRange(requestedRange) };
RangeResults<ArtistId> artists; db::RangeResults<db::ArtistId> artists;
switch (getMode()) switch (getMode())
{ {
@@ -56,7 +54,7 @@ namespace lms::ui
params.setUser(LmsApp->getUserId()); params.setUser(LmsApp->getUserId());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType); params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::StarredDateDesc); params.setSortMethod(db::ArtistSortMethod::StarredDateDesc);
params.setRange(range); params.setRange(range);
artists = feedbackService.findStarredArtists(params); artists = feedbackService.findStarredArtists(params);
break; break;
@@ -90,48 +88,48 @@ namespace lms::ui
case Mode::RecentlyAdded: case Mode::RecentlyAdded:
{ {
Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType); params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::AddedDesc); params.setSortMethod(db::ArtistSortMethod::AddedDesc);
params.setRange(range); params.setRange(range);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
artists = Artist::findIds(LmsApp->getDbSession(), params); artists = db::Artist::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
case Mode::RecentlyModified: case Mode::RecentlyModified:
{ {
Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType); params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::LastWrittenDesc); params.setSortMethod(db::ArtistSortMethod::LastWrittenDesc);
params.setRange(range); params.setRange(range);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
artists = Artist::findIds(LmsApp->getDbSession(), params); artists = db::Artist::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
case Mode::All: case Mode::All:
{ {
Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType); params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::SortName); params.setSortMethod(db::ArtistSortMethod::SortName);
params.setRange(range); params.setRange(range);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
artists = Artist::findIds(LmsApp->getDbSession(), params); artists = db::Artist::findIds(LmsApp->getDbSession(), params);
} }
break; break;
} }
@@ -143,22 +141,22 @@ namespace lms::ui
return artists; return artists;
} }
RangeResults<db::ArtistId> ArtistCollector::getRandomArtists(Range range) db::RangeResults<db::ArtistId> ArtistCollector::getRandomArtists(Range range)
{ {
assert(getMode() == Mode::Random); assert(getMode() == Mode::Random);
if (!_randomArtists) if (!_randomArtists)
{ {
Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType); params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::Random); params.setSortMethod(db::ArtistSortMethod::Random);
params.setRange(Range{ 0, getMaxCount() }); params.setRange(db::Range{ 0, getMaxCount() });
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
_randomArtists = Artist::findIds(LmsApp->getDbSession(), params); _randomArtists = db::Artist::findIds(LmsApp->getDbSession(), params);
} }
} }
+30 -32
View File
@@ -47,11 +47,9 @@
namespace lms::ui namespace lms::ui
{ {
using namespace db;
namespace namespace
{ {
std::optional<ArtistId> extractArtistIdFromInternalPath() std::optional<db::ArtistId> extractArtistIdFromInternalPath()
{ {
if (wApp->internalPathMatches("/artist/mbid/")) if (wApp->internalPathMatches("/artist/mbid/"))
{ {
@@ -66,7 +64,7 @@ namespace lms::ui
return std::nullopt; return std::nullopt;
} }
return core::stringUtils::readAs<ArtistId::ValueType>(wApp->internalPathNextPart("/artist/")); return core::stringUtils::readAs<db::ArtistId::ValueType>(wApp->internalPathNextPart("/artist/"));
} }
} // namespace } // namespace
@@ -109,7 +107,7 @@ namespace lms::ui
if (!artistId) if (!artistId)
throw ArtistNotFoundException{}; throw ArtistNotFoundException{};
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, 6) }; const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist, db::TrackArtistLinkType::ReleaseArtist }, 6) };
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -131,7 +129,7 @@ namespace lms::ui
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") }; Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{ {
auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results }; auto clusterTypes{ db::ClusterType::findIds(LmsApp->getDbSession()).results };
auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) }; auto clusterGroups{ artist->getClusterGroups(clusterTypes, 3) };
for (const auto& clusters : clusterGroups) for (const auto& clusters : clusterGroups)
@@ -232,16 +230,16 @@ namespace lms::ui
{ {
_releaseContainers.clear(); _releaseContainers.clear();
Release::FindParameters params; db::Release::FindParameters params;
params.setFilters(_filters.getDbFilters()); params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {}); params.setArtist(_artistId, { db::TrackArtistLinkType::ReleaseArtist }, {});
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod()); params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) }; const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) };
if (!releases.results.empty()) if (!releases.results.empty())
{ {
// first pass: gather all ids and sort by release type // first pass: gather all ids and sort by release type
for (const ReleaseId releaseId : releases.results) for (const db::ReleaseId releaseId : releases.results)
{ {
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) }; const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) };
@@ -274,27 +272,27 @@ namespace lms::ui
void Artist::refreshAppearsOnReleases() void Artist::refreshAppearsOnReleases()
{ {
constexpr core::EnumSet<TrackArtistLinkType> types{ constexpr core::EnumSet<db::TrackArtistLinkType> types{
TrackArtistLinkType::Artist, db::TrackArtistLinkType::Artist,
TrackArtistLinkType::Arranger, db::TrackArtistLinkType::Arranger,
TrackArtistLinkType::Composer, db::TrackArtistLinkType::Composer,
TrackArtistLinkType::Conductor, db::TrackArtistLinkType::Conductor,
TrackArtistLinkType::Lyricist, db::TrackArtistLinkType::Lyricist,
TrackArtistLinkType::Mixer, db::TrackArtistLinkType::Mixer,
TrackArtistLinkType::Performer, db::TrackArtistLinkType::Performer,
TrackArtistLinkType::Producer, db::TrackArtistLinkType::Producer,
TrackArtistLinkType::Remixer, db::TrackArtistLinkType::Remixer,
TrackArtistLinkType::Writer, db::TrackArtistLinkType::Writer,
}; };
_appearsOnReleaseContainer = {}; _appearsOnReleaseContainer = {};
Release::FindParameters params; db::Release::FindParameters params;
params.setFilters(_filters.getDbFilters()); params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId, types, { TrackArtistLinkType::ReleaseArtist }); params.setArtist(_artistId, types, { db::TrackArtistLinkType::ReleaseArtist });
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc); params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) }; const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) };
if (!releases.results.empty()) if (!releases.results.empty())
{ {
Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) }; Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) };
@@ -323,7 +321,7 @@ namespace lms::ui
setCondition("if-has-non-release-tracks", added); setCondition("if-has-non-release-tracks", added);
} }
void Artist::refreshSimilarArtists(const std::vector<ArtistId>& similarArtistsId) void Artist::refreshSimilarArtists(const std::vector<db::ArtistId>& similarArtistsId)
{ {
if (similarArtistsId.empty()) if (similarArtistsId.empty())
return; return;
@@ -331,7 +329,7 @@ namespace lms::ui
setCondition("if-has-similar-artists", true); setCondition("if-has-similar-artists", true);
Wt::WContainerWidget* similarArtistsContainer{ bindNew<Wt::WContainerWidget>("similar-artists") }; Wt::WContainerWidget* similarArtistsContainer{ bindNew<Wt::WContainerWidget>("similar-artists") };
for (const ArtistId artistId : similarArtistsId) for (const db::ArtistId artistId : similarArtistsId)
{ {
const db::Artist::pointer similarArtist{ db::Artist::find(LmsApp->getDbSession(), artistId) }; const db::Artist::pointer similarArtist{ db::Artist::find(LmsApp->getDbSession(), artistId) };
if (!similarArtist) if (!similarArtist)
@@ -371,19 +369,19 @@ namespace lms::ui
{ {
bool areTracksAdded{}; bool areTracksAdded{};
const Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize }; const db::Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
Track::FindParameters params; db::Track::FindParameters params;
params.setFilters(_filters.getDbFilters()); params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId); params.setArtist(_artistId);
params.setRange(range); params.setRange(range);
params.setSortMethod(TrackSortMethod::Name); params.setSortMethod(db::TrackSortMethod::Name);
params.setNonRelease(true); params.setNonRelease(true);
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const auto tracks{ Track::find(LmsApp->getDbSession(), params) }; const auto tracks{ db::Track::find(LmsApp->getDbSession(), params) };
for (const Track::pointer& track : tracks.results) for (const db::Track::pointer& track : tracks.results)
{ {
// TODO handle this with range // TODO handle this with range
if (_trackContainer->getCount() == _tracksMaxCount) if (_trackContainer->getCount() == _tracksMaxCount)
+14 -27
View File
@@ -61,14 +61,13 @@ namespace lms::ui
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount)
{ {
using namespace db;
assert(maxTrackCount); assert(maxTrackCount);
std::vector<TrackId> res; std::vector<db::TrackId> res;
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
for (const ReleaseId releaseId : releasesId) for (const db::ReleaseId releaseId : releasesId)
{ {
db::Track::FindParameters params; db::Track::FindParameters params;
params.setRelease(releaseId); params.setRelease(releaseId);
@@ -88,39 +87,27 @@ namespace lms::ui
return res; return res;
} }
std::vector<db::TrackId> getDiscTracks(db::Session& session, const std::vector<PlayQueueController::Disc>& discs, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getMediumTracks(db::Session& session, db::MediumId medium, const Filters& filters, std::size_t maxTrackCount)
{ {
using namespace db;
assert(maxTrackCount); assert(maxTrackCount);
std::vector<TrackId> res; std::vector<db::TrackId> res;
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
for (const PlayQueueController::Disc& disc : discs) db::Track::FindParameters params;
{ params.setMedium(medium);
db::Track::FindParameters params; params.setSortMethod(db::TrackSortMethod::TrackNumber);
params.setRelease(disc.releaseId); params.setFilters(filters.getDbFilters());
params.setSortMethod(db::TrackSortMethod::Release); params.setRange(db::Range{ .offset = 0, .size = maxTrackCount });
params.setDiscNumber(disc.discNumber);
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
const auto tracks{ db::Track::findIds(session, params) }; const auto tracks{ db::Track::findIds(session, params) };
res.reserve(res.size() + tracks.results.size()); return tracks.results;
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
if (res.size() == maxTrackCount)
break;
}
return res;
} }
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
{ {
using namespace db;
assert(maxTrackCount); assert(maxTrackCount);
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -129,7 +116,7 @@ namespace lms::ui
params.setTrackList(trackListId); params.setTrackList(trackListId);
params.setFilters(filters.getDbFilters()); params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount }); params.setRange(db::Range{ 0, maxTrackCount });
params.setSortMethod(TrackSortMethod::TrackList); params.setSortMethod(db::TrackSortMethod::TrackList);
return db::Track::findIds(session, params).results; return db::Track::findIds(session, params).results;
} }
@@ -179,9 +166,9 @@ namespace lms::ui
processCommand(command, tracks); processCommand(command, tracks);
} }
void PlayQueueController::processCommand(Command command, const std::vector<Disc>& discs) void PlayQueueController::processCommand(Command command, db::MediumId medium)
{ {
const std::vector<db::TrackId> tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters, _maxTrackCountToEnqueue) }; const std::vector<db::TrackId> tracks{ getMediumTracks(LmsApp->getDbSession(), medium, _filters, _maxTrackCountToEnqueue) };
processCommand(command, tracks); processCommand(command, tracks);
} }

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