Merge branch 'develop' for release v3.67.0
This commit is contained in:
@@ -48,7 +48,7 @@ _LMS_ supports an Artist information folder to manage metadata and images for ar
|
||||
|
||||
The folder must follow a structure defined by Kodi, as detailed [here](https://kodi.wiki/view/Artist_information_folder). `artist.nfo` files are used to define additional artist information such as biography, sort name, and MusicBrainz ArtistID. See the format [here](https://kodi.wiki/view/NFO_files/Artists).
|
||||
|
||||
__Note__: `artist.nfo` files must include both `name` and `musicBrainzArtistID` fields to be recognized.
|
||||
__Note__: If no name is provided in the `artist.nfo` file, the name of the containing folder is used.
|
||||
|
||||
### Filtering
|
||||
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
|
||||
|
||||
@@ -78,6 +78,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
${artist-image-fallback-to-release class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:artist-image-fallback-to-release}">
|
||||
${tr:Lms.Admin.Database.artist-image-fallback-to-release}
|
||||
</label>
|
||||
<div class="invalid-feedback">
|
||||
${artist-image-fallback-to-release-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:similarity-engine-type}">
|
||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<message id="Lms.Explore.Artist.template">
|
||||
<div class="row mb-3 gy-3">
|
||||
<div class="col-lg-3 col-md-4">
|
||||
${artwork class="Lms-cursor-pointer rounded d-block mx-auto"}
|
||||
${artwork class="rounded d-block mx-auto"}
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2>${name}</h2>
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Artists.template.entry">
|
||||
<div class="col h-100 Lms-bg-dark-hover rounded">
|
||||
<div class="col h-100 Lms-bg-dark-hover">
|
||||
<div class="p-2">
|
||||
<div class="Lms-responsive-square Lms-cover-container mb-1">
|
||||
<div class="Lms-responsive-circle Lms-cover-container mb-1">
|
||||
${image class="shadow-sm"}
|
||||
</div>
|
||||
${name class="d-block text-center text-truncate text-decoration-none link-secondary"}
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Allow merging artists without an MBID to those with one</message>
|
||||
<message id="Lms.Admin.Database.artist-image-fallback-to-release">Use an album artwork if no artist image is available</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimiters to be used for splitting artist tags</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artists to not split using the delimiters (one artist per line)</message>
|
||||
<message id="Lms.Admin.Database.daily">Daily</message>
|
||||
@@ -100,9 +101,10 @@
|
||||
|
||||
<!--Scanner Controller-->
|
||||
<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.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-file">Cannot read file</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-lyrics-file">Cannot parse lyrics file</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">Cannot parse playlist file</message>
|
||||
@@ -115,6 +117,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} files were scanned in {2} at {4} on {3} (UTC) - {5} errors, {6} duplicates</message>
|
||||
<message id="Lms.Admin.ScannerController.missing-artist-name">Missing artist name</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Unable to find track paths</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-path-missing">Unable to find the path "{1}"</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">No audio track</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated track MBID</message>
|
||||
@@ -129,6 +134,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associating playlist tracks: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release images: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-track-images">Associating track images: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Permitir la fusión de artistas sin MBID con aquellos que sí lo tienen</message>
|
||||
<message id="Lms.Admin.Database.artist-image-fallback-to-release">Usar una imagen del álbum si no hay una imagen disponible del artista</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimitadores usados para separar las etiquetas de los artistas</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artistas que no se deben separar usando los delimitadores (un artista por línea)</message>
|
||||
<message id="Lms.Admin.Database.daily">Diariamente</message>
|
||||
@@ -100,9 +101,10 @@
|
||||
|
||||
<!--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-embedded-image">No se puede leer la imagen en el índice {1}</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-file">No ha sido posible leer el fichero</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-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>
|
||||
@@ -115,6 +117,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message>
|
||||
<message id="Lms.Admin.ScannerController.missing-artist-name">Falta el nombre del artista</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">No se pudieron encontrar las rutas de las pistas</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-path-missing">No se pudo encontrar la ruta "{1}"</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">No es una pista de audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash del fichero duplicado</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">MBID de la pista duplicado</message>
|
||||
@@ -129,6 +134,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Asignando imágenes de los álbumes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-track-images">Asignando imágenes de las pistas: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Permettre la fusion des artistes sans MBID avec ceux qui en ont un</message>
|
||||
<message id="Lms.Admin.Database.artist-image-fallback-to-release">Utilizza un'immagine dell'album se non è disponibile un'immagine dell'artista</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Délimiteurs à utiliser pour séparer les tags d'artistes</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artistes à ne pas séparer en utilisant les délimiteurs (un artiste par ligne)</message>
|
||||
<message id="Lms.Admin.Database.daily">Tous les jours</message>
|
||||
@@ -100,9 +101,10 @@
|
||||
|
||||
<!--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-embedded-image">Impossible de lire l'image à l'indice {1}</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-file">Impossible de lire le fichier</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-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>
|
||||
@@ -115,6 +117,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers analysés en {2} à {4} le {3} (UTC) - {5} erreurs, {6} duplicatas</message>
|
||||
<message id="Lms.Admin.ScannerController.missing-artist-name">Nom de l'artiste manquant</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossible de trouver les chemins des pistes</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossible de trouver le chemin "{1}"</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">Pas de piste audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID dupliqué</message>
|
||||
@@ -129,6 +134,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-track-images">Association des images des pistes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Consentire la fusione degli artisti senza MBID con quelli che ne hanno uno</message>
|
||||
<message id="Lms.Admin.Database.artist-image-fallback-to-release">Usa l'immagine dell'album se non è disponibile un'immagine dell'artista</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimitatori da utilizzare per separare i tag degli artisti</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artisti da non suddividere utilizzando i delimitatori (un artista per riga)</message>
|
||||
<message id="Lms.Admin.Database.daily">Giornaliera</message>
|
||||
@@ -100,9 +101,10 @@
|
||||
|
||||
<!--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-embedded-image">Impossibile leggere l'immagine all'indice {1}</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-file">Non in grado di leggere il file</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-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>
|
||||
@@ -115,6 +117,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} file scansionati in {2} alle {3} il {4} (UTC) - {5} errori, {6} duplicati</message>
|
||||
<message id="Lms.Admin.ScannerController.missing-artist-name">Nome dell'artista mancante</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Impossibile trovare i percorsi delle tracce</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-path-missing">Impossibile trovare il percorso "{1}"</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">Nessuna traccia audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID doppio</message>
|
||||
@@ -129,6 +134,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associando brani della playlist: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-track-images">Associando immagini delle tracce: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Pozwól na łączenie artystów bez MBID z tymi, którzy go mają</message>
|
||||
<message id="Lms.Admin.Database.artist-image-fallback-to-release">Użyj okładki albumu, jeśli zdjęcie artysty nie jest dostępne</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Znaki rozdzielające artystów</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artyści, których nie należy dzielić przy użyciu separatorów (jeden artysta na linię)</message>
|
||||
<message id="Lms.Admin.Database.daily">Codziennie</message>
|
||||
@@ -101,9 +102,10 @@
|
||||
|
||||
<!--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-embedded-image">Nie można odczytać obrazu pod indeksem {1}</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-file">Nie udało się odczytać pliku</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-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>
|
||||
@@ -124,6 +126,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">Przeskanowano {1} plików w {2} o {4} dnia {3} (UTC) - {5} błędów, {6} duplikatów</message>
|
||||
<message id="Lms.Admin.ScannerController.missing-artist-name">Brakuje nazwy artysty</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">Nie można znaleźć ścieżek utworów</message>
|
||||
<message id="Lms.Admin.ScannerController.playlist-path-missing">Nie można znaleźć ścieżki „{1}”</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">Brak ścieżki dźwiękowej</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Zduplikowny hash pliku</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Zduplikowany MBID pliku</message>
|
||||
@@ -138,6 +143,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Kojarzenie utworów z playlisty: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-track-images">Kojarzenie obrazów ścieżek: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
||||
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
||||
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<!--Scan settings-->
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Admin.Database.daily">每日</message>
|
||||
|
||||
|
||||
@@ -100,9 +101,10 @@
|
||||
<!--Scanner Controller-->
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
||||
|
||||
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
|
||||
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件 ({1})</message>
|
||||
|
||||
|
||||
|
||||
@@ -114,6 +116,9 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message>
|
||||
|
||||
|
||||
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message>
|
||||
|
||||
+7
-4
@@ -9,7 +9,7 @@
|
||||
<message id="Lms.Explore.Release.template">
|
||||
<div class="row mb-3 gy-3">
|
||||
<div class="col-lg-3 col-md-4">
|
||||
${cover class="Lms-cursor-pointer rounded d-block mx-auto"}
|
||||
${artwork class="rounded d-block mx-auto"}
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2>${name}${<if-has-release-comment>}<span class="ms-1 text-muted">[${comment}]</span>${</if-has-release-comment>}</h2>
|
||||
@@ -54,8 +54,11 @@
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-disc">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="flex-fill">${disc-title}</h4>
|
||||
<h4 class="d-flex align-items-center">
|
||||
${<if-has-artwork>}
|
||||
<div class="p-1">${artwork class="Lms-cursor-pointer"}</div>
|
||||
${</if-has-artwork>}
|
||||
<div class="p-2 flex-fill">${disc-title}</div>
|
||||
<div class="p-2 d-flex">
|
||||
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||
<div class="dropdown d-inline-block">
|
||||
@@ -68,7 +71,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
${tracks class="d-grid gap-1 Lms-row-container"}
|
||||
</message>
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ ffmpeg-file = "/usr/bin/ffmpeg";
|
||||
log-file = "";
|
||||
access-log-file = "";
|
||||
# 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
|
||||
log-min-severity = "info";
|
||||
# Database consistency check to run at startup.
|
||||
# Can be "none", "quick", or "full"
|
||||
db-integrity-check = "quick";
|
||||
# Output db queries on stdout
|
||||
db-show-queries = false;
|
||||
|
||||
|
||||
@@ -110,6 +110,15 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Lms-image-artist {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#lms-mp-progress {
|
||||
transition: none;
|
||||
}
|
||||
@@ -184,6 +193,18 @@ body {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.Lms-responsive-circle {
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Lms-responsive-circle:after {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
.Lms-responsive-square {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
|
||||
add_library(lmsav STATIC
|
||||
impl/AudioFile.cpp
|
||||
impl/RawResourceHandlerCreator.cpp
|
||||
impl/Transcoder.cpp
|
||||
impl/TranscodingResourceHandler.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsav INTERFACE
|
||||
|
||||
@@ -34,7 +34,7 @@ extern "C"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "av/Types.hpp"
|
||||
#include "av/Exception.hpp"
|
||||
|
||||
namespace lms::av
|
||||
{
|
||||
@@ -321,42 +321,4 @@ namespace lms::av
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string_view getMimeType(const std::filesystem::path& fileExtension)
|
||||
{
|
||||
// List should be sync with the demuxers shipped in the lms's docker version
|
||||
// + the _audioFileExtensions in ScanSettings
|
||||
// std::filesystem::path does not seem to have std::hash specialization on freebsd
|
||||
static const std::unordered_map<std::string, std::string_view> entries{
|
||||
{ ".mp3", "audio/mpeg" },
|
||||
{ ".ogg", "audio/ogg" },
|
||||
{ ".oga", "audio/ogg" },
|
||||
{ ".opus", "audio/opus" },
|
||||
{ ".aac", "audio/aac" },
|
||||
{ ".alac", "audio/mp4" },
|
||||
{ ".m4a", "audio/mp4" },
|
||||
{ ".m4b", "audio/mp4" },
|
||||
{ ".flac", "audio/flac" },
|
||||
{ ".webm", "audio/webm" },
|
||||
{ ".wav", "audio/x-wav" },
|
||||
{ ".wma", "audio/x-ms-wma" },
|
||||
{ ".ape", "audio/x-monkeys-audio" },
|
||||
{ ".mpc", "audio/x-musepack" },
|
||||
{ ".shn", "audio/x-shn" },
|
||||
{ ".aif", "audio/x-aiff" },
|
||||
{ ".aiff", "audio/x-aiff" },
|
||||
{ ".m3u", "audio/x-mpegurl" },
|
||||
{ ".pls", "audio/x-scpls" },
|
||||
{ ".dsf", "audio/x-dsd" },
|
||||
{ ".wv", "audio/x-wavpack" },
|
||||
{ ".wvp", "audio/x-wavpack" },
|
||||
{ ".mka", "audio/x-matroska" },
|
||||
};
|
||||
|
||||
auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.c_str())) };
|
||||
if (it == std::cend(entries))
|
||||
return "";
|
||||
|
||||
return it->second;
|
||||
}
|
||||
} // namespace lms::av
|
||||
|
||||
@@ -27,35 +27,20 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
#include "av/Types.hpp"
|
||||
#include "av/Exception.hpp"
|
||||
|
||||
namespace lms::av::transcoding
|
||||
namespace lms::av
|
||||
{
|
||||
|
||||
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
|
||||
{
|
||||
return std::make_unique<Transcoder>(inputParameters, outputParameters);
|
||||
}
|
||||
|
||||
static std::atomic<size_t> globalId{};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
std::string_view formatToMimetype(OutputFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
return "audio/mpeg";
|
||||
case OutputFormat::OGG_OPUS:
|
||||
return "audio/opus";
|
||||
case OutputFormat::MATROSKA_OPUS:
|
||||
return "audio/x-matroska";
|
||||
case OutputFormat::OGG_VORBIS:
|
||||
return "audio/ogg";
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
return "audio/webm";
|
||||
}
|
||||
|
||||
throw Exception{ "Invalid encoding" };
|
||||
}
|
||||
|
||||
void Transcoder::init()
|
||||
{
|
||||
ffmpegPath = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
@@ -63,10 +48,10 @@ namespace lms::av::transcoding
|
||||
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
|
||||
Transcoder::Transcoder(const InputParameters& inputParams, const OutputParameters& outputParams)
|
||||
: _debugId{ globalId++ }
|
||||
, _inputParameters{ inputParameters }
|
||||
, _outputParameters{ outputParameters }
|
||||
, _inputParams{ inputParams }
|
||||
, _outputParams{ outputParams }
|
||||
{
|
||||
start();
|
||||
}
|
||||
@@ -80,17 +65,18 @@ namespace lms::av::transcoding
|
||||
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(_inputParameters.trackPath))
|
||||
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
|
||||
if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
|
||||
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
|
||||
if (!std::filesystem::exists(_inputParams.file))
|
||||
throw Exception{ "File " + _inputParams.file.string() + " does not exist!" };
|
||||
if (!std::filesystem::is_regular_file(_inputParams.file))
|
||||
throw Exception{ "File " + _inputParams.file.string() + " is not regular!" };
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
|
||||
// TODO store/raise e.code()
|
||||
throw Exception{ "File error '" + _inputParams.file.string() + "': " + e.what() };
|
||||
}
|
||||
|
||||
LOG(INFO, "Transcoding file " << _inputParameters.trackPath);
|
||||
LOG(INFO, "Transcoding file " << _inputParams.file);
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
@@ -109,22 +95,22 @@ namespace lms::av::transcoding
|
||||
args.emplace_back("-ss");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1'000 });
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_inputParams.offset.count() / float{ 1'000 });
|
||||
args.emplace_back(oss.str());
|
||||
}
|
||||
|
||||
// Input file
|
||||
args.emplace_back("-i");
|
||||
args.emplace_back(_inputParameters.trackPath.string());
|
||||
args.emplace_back(_inputParams.file.string());
|
||||
|
||||
// Stream mapping, if set
|
||||
if (_outputParameters.stream)
|
||||
if (_inputParams.streamIndex)
|
||||
{
|
||||
args.emplace_back("-map");
|
||||
args.emplace_back("0:" + std::to_string(*_outputParameters.stream));
|
||||
args.emplace_back("0:" + std::to_string(*_inputParams.streamIndex));
|
||||
}
|
||||
|
||||
if (_outputParameters.stripMetadata)
|
||||
if (_outputParams.stripMetadata)
|
||||
{
|
||||
// Strip metadata
|
||||
args.emplace_back("-map_metadata");
|
||||
@@ -136,10 +122,10 @@ namespace lms::av::transcoding
|
||||
|
||||
// Output bitrates
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_outputParameters.bitrate));
|
||||
args.emplace_back(std::to_string(_outputParams.bitrate));
|
||||
|
||||
// Codecs and formats
|
||||
switch (_outputParameters.format)
|
||||
switch (_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
args.emplace_back("-f");
|
||||
@@ -175,11 +161,9 @@ namespace lms::av::transcoding
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParameters.format)) + ")" };
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParams.format)) + ")" };
|
||||
}
|
||||
|
||||
_outputMimeType = formatToMimetype(_outputParameters.format);
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
|
||||
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||
@@ -213,6 +197,25 @@ namespace lms::av::transcoding
|
||||
return _childProcess->readSome(buffer, bufferSize);
|
||||
}
|
||||
|
||||
std::string_view Transcoder::getOutputMimeType() const
|
||||
{
|
||||
switch (_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
return "audio/mpeg";
|
||||
case OutputFormat::OGG_OPUS:
|
||||
return "audio/opus";
|
||||
case OutputFormat::MATROSKA_OPUS:
|
||||
return "audio/x-matroska";
|
||||
case OutputFormat::OGG_VORBIS:
|
||||
return "audio/ogg";
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
return "audio/webm";
|
||||
}
|
||||
|
||||
return "application/octet-stream"; // default, should not happen
|
||||
}
|
||||
|
||||
bool Transcoder::finished() const
|
||||
{
|
||||
assert(_childProcess);
|
||||
@@ -220,4 +223,4 @@ namespace lms::av::transcoding
|
||||
return _childProcess->finished();
|
||||
}
|
||||
|
||||
} // namespace lms::av::transcoding
|
||||
} // namespace lms::av
|
||||
|
||||
@@ -19,47 +19,37 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "av/TranscodingParameters.hpp"
|
||||
#include "av/ITranscoder.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
class IChildProcess;
|
||||
}
|
||||
|
||||
namespace lms::av::transcoding
|
||||
namespace lms::av
|
||||
{
|
||||
class Transcoder
|
||||
class Transcoder : public ITranscoder
|
||||
{
|
||||
public:
|
||||
Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
||||
~Transcoder();
|
||||
~Transcoder() override;
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
Transcoder& operator=(const Transcoder&) = delete;
|
||||
Transcoder(Transcoder&&) = delete;
|
||||
Transcoder& operator=(Transcoder&&) = delete;
|
||||
|
||||
// non blocking calls
|
||||
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
|
||||
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
|
||||
|
||||
const std::string& getOutputMimeType() const { return _outputMimeType; }
|
||||
const OutputParameters& getOutputParameters() const { return _outputParameters; }
|
||||
|
||||
bool finished() const;
|
||||
|
||||
private:
|
||||
static void init();
|
||||
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback) override;
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
|
||||
|
||||
std::string_view getOutputMimeType() const override;
|
||||
const OutputParameters& getOutputParameters() const override { return _outputParams; }
|
||||
|
||||
bool finished() const override;
|
||||
static void init();
|
||||
void start();
|
||||
|
||||
const std::size_t _debugId{};
|
||||
const InputParameters _inputParameters;
|
||||
const OutputParameters _outputParameters;
|
||||
std::string _outputMimeType;
|
||||
|
||||
const InputParameters _inputParams;
|
||||
const OutputParameters _outputParams;
|
||||
std::unique_ptr<core::IChildProcess> _childProcess;
|
||||
};
|
||||
} // namespace lms::av::transcoding
|
||||
} // namespace lms::av
|
||||
@@ -101,13 +101,4 @@ namespace lms::av
|
||||
};
|
||||
|
||||
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
|
||||
|
||||
struct AudioFileFormat
|
||||
{
|
||||
std::string mimeType;
|
||||
std::string format;
|
||||
};
|
||||
|
||||
std::string_view getMimeType(const std::filesystem::path& fileExtension);
|
||||
|
||||
} // namespace lms::av
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::av
|
||||
{
|
||||
struct InputParameters
|
||||
{
|
||||
std::filesystem::path file; // Path to the input file
|
||||
std::chrono::milliseconds offset{}; // Offset in the input file to start transcoding from
|
||||
std::optional<std::size_t> streamIndex; // Index of the stream to be transcoded (select "best" audio stream if not set)
|
||||
};
|
||||
|
||||
enum class OutputFormat
|
||||
{
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
struct OutputParameters
|
||||
{
|
||||
OutputFormat format;
|
||||
std::size_t bitrate{ 128'000 };
|
||||
bool stripMetadata{ true };
|
||||
};
|
||||
|
||||
class ITranscoder
|
||||
{
|
||||
public:
|
||||
virtual ~ITranscoder() = default;
|
||||
|
||||
// non blocking calls
|
||||
using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
|
||||
virtual void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback callback) = 0;
|
||||
virtual std::size_t readSome(std::byte* buffer, std::size_t bufferSize) = 0;
|
||||
|
||||
virtual std::string_view getOutputMimeType() const = 0;
|
||||
virtual const OutputParameters& getOutputParameters() const = 0;
|
||||
|
||||
virtual bool finished() const = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
|
||||
} // namespace lms::av
|
||||
@@ -12,6 +12,7 @@ add_library(lmscore STATIC
|
||||
impl/FileResourceHandler.cpp
|
||||
impl/IOContextRunner.cpp
|
||||
impl/Logger.cpp
|
||||
impl/MimeTypes.cpp
|
||||
impl/NetAddress.cpp
|
||||
impl/PartialDateTime.cpp
|
||||
impl/Path.cpp
|
||||
@@ -31,12 +32,12 @@ target_include_directories(lmscore INTERFACE
|
||||
|
||||
target_include_directories(lmscore PRIVATE
|
||||
include
|
||||
${XXHASH_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(lmscore PRIVATE
|
||||
PkgConfig::Config++
|
||||
PkgConfig::Archive
|
||||
PkgConfig::XXHASH
|
||||
)
|
||||
|
||||
target_link_libraries(lmscore PUBLIC
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::zip
|
||||
{
|
||||
@@ -43,9 +43,13 @@ namespace lms::zip
|
||||
: Exception{ "File '" + p.string() + "': " + std::string{ message } }
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
FileException(const std::filesystem::path& p, std::string_view message, int err)
|
||||
: Exception{ "File '" + p.string() + "': " + std::string{ message } + ": " + core::stringUtils::systemErrorToString(err) }
|
||||
class FileStdException : public FileException
|
||||
{
|
||||
public:
|
||||
FileStdException(const std::filesystem::path& p, std::string_view message, int err)
|
||||
: FileException{ p, std::string{ message } + "': " + std::error_code{ err, std::generic_category() }.message() }
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -240,7 +244,7 @@ namespace lms::zip
|
||||
|
||||
std::ifstream ifs{ _currentEntry->filePath, std::ios_base::binary };
|
||||
if (!ifs)
|
||||
throw FileException{ _currentEntry->filePath, "cannot open file", errno };
|
||||
throw FileStdException{ _currentEntry->filePath, "cannot open file", errno };
|
||||
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const std::uint64_t fileSize{ static_cast<std::uint64_t>(ifs.tellg()) };
|
||||
@@ -254,10 +258,10 @@ namespace lms::zip
|
||||
|
||||
// read from file
|
||||
if (!ifs.seekg(_currentEntryOffset, std::ios::beg))
|
||||
throw FileException{ _currentEntry->filePath, "seek failed", errno };
|
||||
throw FileStdException{ _currentEntry->filePath, "seek failed", errno };
|
||||
|
||||
if (!ifs.read(reinterpret_cast<char*>(_readBuffer.data()), bytesToRead))
|
||||
throw FileException{ _currentEntry->filePath, "read failed", errno };
|
||||
throw FileStdException{ _currentEntry->filePath, "read failed", errno };
|
||||
|
||||
const std::uint64_t actualBytesRead{ static_cast<std::uint64_t>(ifs.gcount()) };
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
#include "ChildProcess.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
@@ -28,14 +26,15 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <system_error>
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/read.hpp>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
@@ -44,8 +43,8 @@ namespace lms::core
|
||||
class SystemException : public ChildProcessException
|
||||
{
|
||||
public:
|
||||
SystemException(int err, const std::string& errMsg)
|
||||
: ChildProcessException{ errMsg + ": " + stringUtils::systemErrorToString(err) }
|
||||
SystemException(std::error_code err, const std::string& errMsg)
|
||||
: ChildProcessException{ errMsg + ": " + err.message() }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -63,38 +62,29 @@ namespace lms::core
|
||||
{
|
||||
// make sure only one thread is executing this part of code
|
||||
static std::mutex mutex;
|
||||
std::unique_lock<std::mutex> lock{ mutex };
|
||||
const std::scoped_lock lock{ mutex };
|
||||
|
||||
int pipefd[2];
|
||||
|
||||
// Use 'pipe' instead of 'pipe2', more portable
|
||||
if (pipe(pipefd) < 0)
|
||||
throw SystemException{ errno, "pipe failed!" };
|
||||
if (pipe(pipefd) == -1)
|
||||
throw SystemException{ std::error_code{ errno, std::generic_category() }, "pipe failed!" };
|
||||
|
||||
// Manually set the O_NONBLOCK and O_CLOEXEC flags for both ends of the pipe
|
||||
// Only set O_NONBLOCK on read end - usually programs don't expect stdout to be non-blocking
|
||||
if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1)
|
||||
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
|
||||
|
||||
if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1)
|
||||
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
|
||||
|
||||
if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1)
|
||||
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
|
||||
|
||||
if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1)
|
||||
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
|
||||
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fcntl failed to set O_NONBLOCK!" };
|
||||
|
||||
#if defined(__linux__) && defined(F_SETPIPE_SZ)
|
||||
for (const int fd : { pipefd[0], pipefd[1] })
|
||||
{
|
||||
constexpr std::size_t targetPipeSize{ static_cast<long>(65'536) * 4 };
|
||||
std::size_t currentPipeSize{ 65'536 }; // common default value
|
||||
constexpr int targetPipeSize{ 65'536 * 4 };
|
||||
int currentPipeSize{ 65'536 }; // common default value
|
||||
#if defined(F_GETPIPE_SZ)
|
||||
const int pipeSizeRes{ fcntl(fd, F_GETPIPE_SZ) };
|
||||
if (pipeSizeRes == -1)
|
||||
{
|
||||
const int err{ errno };
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_GETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -103,36 +93,45 @@ namespace lms::core
|
||||
#endif
|
||||
if (currentPipeSize < targetPipeSize)
|
||||
{
|
||||
if (fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1)
|
||||
if (::fcntl(fd, F_SETPIPE_SZ, targetPipeSize) == -1)
|
||||
{
|
||||
const int err{ errno };
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << stringUtils::systemErrorToString(err));
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "F_SETPIPE_SZ failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int res{ fork() };
|
||||
const int res{ fork() };
|
||||
if (res == -1)
|
||||
throw SystemException{ errno, "fork failed!" };
|
||||
throw SystemException{ std::error_code{ errno, std::generic_category() }, "fork failed!" };
|
||||
|
||||
if (res == 0) // CHILD
|
||||
{
|
||||
close(pipefd[0]);
|
||||
close(STDIN_FILENO);
|
||||
close(STDERR_FILENO);
|
||||
// Never close stdin/out/err, most programs expect these to exist;
|
||||
// rather connect them to /dev/null if unwanted
|
||||
const int nullFd{ open("/dev/null", O_RDWR) };
|
||||
// Ignore errors, worst thing is stderr writes to the same fd as lms
|
||||
if (nullFd != -1)
|
||||
{
|
||||
dup2(nullFd, STDIN_FILENO);
|
||||
dup2(nullFd, STDERR_FILENO);
|
||||
close(nullFd);
|
||||
}
|
||||
|
||||
// Replace stdout with pipe write
|
||||
if (dup2(pipefd[1], STDOUT_FILENO) == -1)
|
||||
exit(-1);
|
||||
// Close pipe: read end not needed, write end was dup2ed
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
|
||||
std::vector<const char*> execArgs;
|
||||
std::transform(std::cbegin(args), std::cend(args), std::back_inserter(execArgs), [](const std::string& arg) { return arg.c_str(); });
|
||||
execArgs.push_back(nullptr);
|
||||
|
||||
res = execv(path.string().c_str(), (char* const*)&execArgs[0]);
|
||||
if (res == -1)
|
||||
exit(-1);
|
||||
execv(path.string().c_str(), (char* const*)&execArgs[0]);
|
||||
exit(-1);
|
||||
}
|
||||
else // PARENT
|
||||
{
|
||||
@@ -141,7 +140,7 @@ namespace lms::core
|
||||
boost::system::error_code assignError;
|
||||
_childStdout.assign(pipefd[0], assignError);
|
||||
if (assignError)
|
||||
throw SystemException{ assignError, "fork failed!" };
|
||||
throw SystemException{ assignError, "assigning read end of pipe to asio stream failed!" };
|
||||
}
|
||||
_childPID = res;
|
||||
}
|
||||
@@ -168,7 +167,10 @@ namespace lms::core
|
||||
// process may already have finished
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Killing child process...");
|
||||
if (::kill(_childPID, SIGKILL) == -1)
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << stringUtils::systemErrorToString(errno));
|
||||
{
|
||||
const int err{ errno };
|
||||
LMS_LOG(CHILDPROCESS, DEBUG, "Kill failed: " << (std::error_code{ err, std::generic_category() }.message()));
|
||||
}
|
||||
}
|
||||
|
||||
bool ChildProcess::wait(bool block)
|
||||
@@ -179,7 +181,7 @@ namespace lms::core
|
||||
const pid_t pid{ waitpid(_childPID, &wstatus, block ? 0 : WNOHANG) };
|
||||
|
||||
if (pid == -1)
|
||||
throw SystemException{ errno, "waitpid failed!" };
|
||||
throw SystemException{ std::error_code{ errno, std::generic_category() }, "waitpid failed!" };
|
||||
if (pid == 0)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -19,50 +19,50 @@
|
||||
|
||||
#include "FileResourceHandler.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/MimeTypes.hpp"
|
||||
|
||||
namespace lms
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
||||
{
|
||||
return std::make_unique<FileResourceHandler>(path, mimeType);
|
||||
return std::make_unique<FileResourceHandler>(path, mimeType.empty() ? getMimeType(path.extension()) : mimeType);
|
||||
}
|
||||
|
||||
FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
|
||||
: _path{ path }
|
||||
, _mimeType{ mimeType }
|
||||
: _mimeType{ mimeType }
|
||||
, _ifs{ path, std::ios::in | std::ios::binary }
|
||||
{
|
||||
if (!_ifs)
|
||||
LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << path);
|
||||
else
|
||||
{
|
||||
_ifs.seekg(0, std::ios::end);
|
||||
if (!_ifs.fail())
|
||||
_fileSize = static_cast<::uint64_t>(_ifs.tellg());
|
||||
LMS_LOG(UTILS, DEBUG, "File " << path << ", fileSize = " << _fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
Wt::Http::ResponseContinuation* FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
::uint64_t startByte{ _offset };
|
||||
std::ifstream ifs{ _path, std::ios::in | std::ios::binary };
|
||||
|
||||
if (startByte == 0)
|
||||
if (_offset == 0)
|
||||
{
|
||||
if (!ifs)
|
||||
if (!_ifs)
|
||||
{
|
||||
LMS_LOG(UTILS, ERROR, "Cannot open file stream for " << _path);
|
||||
response.setStatus(404);
|
||||
return {};
|
||||
}
|
||||
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
|
||||
ifs.seekg(0, std::ios::beg);
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "File " << _path << ", fileSize = " << fileSize);
|
||||
|
||||
response.addHeader("Accept-Ranges", "bytes");
|
||||
|
||||
const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) };
|
||||
const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(_fileSize) };
|
||||
if (!ranges.isSatisfiable())
|
||||
{
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes */" << fileSize;
|
||||
contentRange << "bytes */" << _fileSize;
|
||||
response.setStatus(416); // Requested range not satisfiable
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
|
||||
@@ -75,57 +75,51 @@ namespace lms
|
||||
LMS_LOG(UTILS, DEBUG, "Range requested = " << ranges[0].firstByte() << "-" << ranges[0].lastByte());
|
||||
|
||||
response.setStatus(206);
|
||||
startByte = ranges[0].firstByte();
|
||||
_offset = ranges[0].firstByte();
|
||||
_beyondLastByte = ranges[0].lastByte() + 1;
|
||||
|
||||
std::ostringstream contentRange;
|
||||
contentRange << "bytes " << startByte << "-"
|
||||
<< _beyondLastByte - 1 << "/" << fileSize;
|
||||
contentRange << "bytes " << _offset << "-"
|
||||
<< _beyondLastByte - 1 << "/" << _fileSize;
|
||||
|
||||
response.addHeader("Content-Range", contentRange.str());
|
||||
response.setContentLength(_beyondLastByte - startByte);
|
||||
response.setContentLength(_beyondLastByte - _offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "No range requested");
|
||||
|
||||
response.setStatus(200);
|
||||
_beyondLastByte = fileSize;
|
||||
_beyondLastByte = _fileSize;
|
||||
response.setContentLength(_beyondLastByte);
|
||||
}
|
||||
|
||||
LMS_LOG(UTILS, DEBUG, "Mimetype set to '" << _mimeType << "'");
|
||||
response.setMimeType(_mimeType);
|
||||
}
|
||||
else if (!ifs)
|
||||
{
|
||||
LMS_LOG(UTILS, ERROR, "Cannot reopen file stream for " << _path);
|
||||
return {};
|
||||
}
|
||||
|
||||
ifs.seekg(static_cast<std::istream::pos_type>(startByte));
|
||||
_ifs.seekg(static_cast<std::istream::pos_type>(_offset));
|
||||
} // end initial response setup
|
||||
|
||||
std::vector<char> buf;
|
||||
buf.resize(_chunkSize);
|
||||
::uint64_t restSize = _beyondLastByte - _offset;
|
||||
::uint64_t pieceSize = std::min(restSize, static_cast<::uint64_t>(_chunkSize));
|
||||
|
||||
::uint64_t restSize = _beyondLastByte - startByte;
|
||||
::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size();
|
||||
std::vector<char> buf(pieceSize);
|
||||
|
||||
ifs.read(&buf[0], pieceSize);
|
||||
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
|
||||
_ifs.read(buf.data(), buf.size());
|
||||
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(_ifs.gcount()) };
|
||||
if (actualPieceSize > 0)
|
||||
{
|
||||
response.out().write(&buf[0], actualPieceSize);
|
||||
LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << startByte << "-" << startByte + actualPieceSize - 1 << "");
|
||||
response.out().write(buf.data(), actualPieceSize);
|
||||
LMS_LOG(UTILS, DEBUG, "Written " << actualPieceSize << " bytes, range = " << _offset << "-" << _offset + actualPieceSize - 1 << "");
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(UTILS, DEBUG, "Written 0 byte");
|
||||
}
|
||||
|
||||
if (ifs.good() && actualPieceSize < restSize)
|
||||
if (!_ifs.good())
|
||||
LMS_LOG(UTILS, WARNING, "Error reading from file!");
|
||||
else if (actualPieceSize < restSize)
|
||||
{
|
||||
_offset = startByte + actualPieceSize;
|
||||
_offset += actualPieceSize;
|
||||
LMS_LOG(UTILS, DEBUG, "Job not complete! Remaining range: " << _offset << "-" << _beyondLastByte - 1);
|
||||
|
||||
return response.createContinuation();
|
||||
@@ -134,4 +128,4 @@ namespace lms
|
||||
LMS_LOG(UTILS, DEBUG, "Job complete!");
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace lms
|
||||
} // namespace lms::core
|
||||
@@ -20,12 +20,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms
|
||||
namespace lms::core
|
||||
{
|
||||
class FileResourceHandler final : public IResourceHandler
|
||||
{
|
||||
@@ -38,9 +39,10 @@ namespace lms
|
||||
|
||||
static constexpr std::size_t _chunkSize{ 262'144 };
|
||||
|
||||
std::filesystem::path _path;
|
||||
std::string _mimeType;
|
||||
::uint64_t _beyondLastByte{};
|
||||
::uint64_t _offset{};
|
||||
::uint64_t _fileSize{};
|
||||
std::ifstream _ifs;
|
||||
};
|
||||
} // namespace lms
|
||||
} // namespace lms::core
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "core/MimeTypes.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
std::string_view getMimeType(const std::filesystem::path& fileExtension)
|
||||
{
|
||||
static const std::unordered_map<std::string, std::string_view> entries{
|
||||
// audio
|
||||
{ ".aac", "audio/aac" },
|
||||
{ ".ac3", "audio/ac3" },
|
||||
{ ".aif", "audio/x-aiff" },
|
||||
{ ".aiff", "audio/x-aiff" },
|
||||
{ ".alac", "audio/mp4" },
|
||||
{ ".ape", "audio/x-monkeys-audio" },
|
||||
{ ".dff", "audio/x-dsd-dff" },
|
||||
{ ".dsdiff", "audio/x-dsd-diff" },
|
||||
{ ".dsf", "audio/x-dsd" },
|
||||
{ ".dsf", "audio/x-dsd-dsf" },
|
||||
{ ".dts", "audio/vnd.dts" },
|
||||
{ ".dtshd", "audio/vnd.dts.hd" },
|
||||
{ ".eac3", "audio/eac3" },
|
||||
{ ".flac", "audio/flac" },
|
||||
{ ".m3u", "audio/x-mpegurl" },
|
||||
{ ".m4a", "audio/mp4" },
|
||||
{ ".m4b", "audio/mp4" },
|
||||
{ ".mka", "audio/x-matroska" },
|
||||
{ ".mka", "audio/x-matroska" },
|
||||
{ ".mp3", "audio/mpeg" },
|
||||
{ ".mpc", "audio/x-musepack" },
|
||||
{ ".oga", "audio/ogg" },
|
||||
{ ".ogg", "audio/ogg" },
|
||||
{ ".opus", "audio/opus" },
|
||||
{ ".pls", "audio/x-scpls" },
|
||||
{ ".shn", "audio/x-shn" },
|
||||
{ ".wav", "audio/x-wav" },
|
||||
{ ".webm", "audio/webm" },
|
||||
{ ".wma", "audio/x-ms-wma" },
|
||||
{ ".wv", "audio/x-wavpack" },
|
||||
{ ".wvp", "audio/x-wavpack" },
|
||||
|
||||
// image
|
||||
{ ".bmp", "image/bmp" },
|
||||
{ ".gif", "image/gif" },
|
||||
{ ".jpg", "image/jpeg" },
|
||||
{ ".jpeg", "image/jpeg" },
|
||||
{ ".png", "image/png" },
|
||||
{ ".webp", "image/webp" },
|
||||
};
|
||||
|
||||
auto it{ entries.find(core::stringUtils::stringToLower(fileExtension.c_str())) };
|
||||
if (it == std::cend(entries))
|
||||
return "application/octet-stream";
|
||||
|
||||
return it->second;
|
||||
}
|
||||
} // namespace lms::core
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <fstream>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <system_error>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <boost/tokenizer.hpp>
|
||||
@@ -66,16 +67,24 @@ namespace lms::core::pathUtils
|
||||
return std::filesystem::create_directory(dir);
|
||||
}
|
||||
|
||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file)
|
||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file, std::error_code& ec)
|
||||
{
|
||||
Wt::WDateTime res;
|
||||
|
||||
struct stat sb
|
||||
{
|
||||
};
|
||||
|
||||
if (stat(file.c_str(), &sb) == -1)
|
||||
throw LmsException("Failed to get stats on file '" + file.string() + "'");
|
||||
{
|
||||
ec = std::error_code{ errno, std::generic_category() };
|
||||
}
|
||||
else
|
||||
{
|
||||
ec = std::error_code{};
|
||||
res = Wt::WDateTime::fromTime_t(sb.st_mtime);
|
||||
}
|
||||
|
||||
return Wt::WDateTime::fromTime_t(sb.st_mtime);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <utility>
|
||||
@@ -509,11 +508,4 @@ namespace lms::core::stringUtils
|
||||
|
||||
return "[" + std::to_string(mins) + ":" + (secs < 10 ? "0" : "") + std::to_string(secs) + "." + (millis < 100 ? (millis < 10 ? "00" : "0") : "") + std::to_string(millis) + "]";
|
||||
}
|
||||
|
||||
std::string systemErrorToString(int err)
|
||||
{
|
||||
std::array<char, 128> buffer{};
|
||||
::strerror_r(err, buffer.data(), buffer.size());
|
||||
return std::string{ buffer.data() };
|
||||
}
|
||||
} // namespace lms::core::stringUtils
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType);
|
||||
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType = "");
|
||||
}
|
||||
@@ -22,8 +22,7 @@
|
||||
#include <Wt/Http/Request.h>
|
||||
#include <Wt/Http/Response.h>
|
||||
|
||||
// TODO, move elsewhere
|
||||
namespace lms
|
||||
namespace lms::core
|
||||
{
|
||||
// Helper class to serve a resource (must be saved as continuation data if not complete)
|
||||
class IResourceHandler
|
||||
@@ -34,4 +33,4 @@ namespace lms
|
||||
[[nodiscard]] virtual Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) = 0;
|
||||
virtual void abort() = 0;
|
||||
};
|
||||
} // namespace lms
|
||||
} // namespace lms::core
|
||||
+4
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -20,11 +20,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms::av
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IResourceHandler> createRawResourceHandler(const std::filesystem::path& path);
|
||||
std::string_view getMimeType(const std::filesystem::path& fileExtension);
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::core::pathUtils
|
||||
bool ensureDirectory(const std::filesystem::path& dir);
|
||||
|
||||
// Get the last write time since Epoch
|
||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& dir);
|
||||
Wt::WDateTime getLastWriteTime(const std::filesystem::path& file, std::error_code& ec);
|
||||
|
||||
// returns false if aborted by user
|
||||
bool exploreFilesRecursive(const std::filesystem::path& directory, std::function<bool(std::error_code, const std::filesystem::path&)> cb, const std::filesystem::path* excludeDirFileName = {});
|
||||
|
||||
@@ -119,6 +119,4 @@ namespace lms::core::stringUtils
|
||||
|
||||
// to "[minutes:seconds.milliseconds]"
|
||||
std::string formatTimestamp(std::chrono::milliseconds timestamp);
|
||||
|
||||
std::string systemErrorToString(int err);
|
||||
} // namespace lms::core::stringUtils
|
||||
@@ -1,6 +1,7 @@
|
||||
add_library(lmsdatabase STATIC
|
||||
impl/Artist.cpp
|
||||
impl/ArtistInfo.cpp
|
||||
impl/Artwork.cpp
|
||||
impl/AuthToken.cpp
|
||||
impl/Cluster.cpp
|
||||
impl/Db.cpp
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
@@ -315,9 +315,14 @@ AND NOT EXISTS (
|
||||
return getMBID().has_value();
|
||||
}
|
||||
|
||||
ObjectPtr<Image> Artist::getImage() const
|
||||
ObjectPtr<Artwork> Artist::getPreferredArtwork() const
|
||||
{
|
||||
return ObjectPtr<Image>{ _image };
|
||||
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||
}
|
||||
|
||||
ArtworkId Artist::getPreferredArtworkId() const
|
||||
{
|
||||
return _preferredArtwork.id();
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
@@ -416,8 +421,8 @@ AND NOT EXISTS (
|
||||
LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'");
|
||||
}
|
||||
|
||||
void Artist::setImage(ObjectPtr<Image> image)
|
||||
void Artist::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||
{
|
||||
_image = getDboPtr(image);
|
||||
_preferredArtwork = getDboPtr(artwork);
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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/Artwork.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
Artwork::Artwork(ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage)
|
||||
: _trackEmbeddedImage{ getDboPtr(trackEmbeddedImage) }
|
||||
{
|
||||
}
|
||||
|
||||
Artwork::Artwork(ObjectPtr<Image> image)
|
||||
: _image{ getDboPtr(image) }
|
||||
{
|
||||
}
|
||||
|
||||
Artwork::pointer Artwork::create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
return session.getDboSession()->add(std::unique_ptr<Artwork>{ new Artwork{ trackEmbeddedImage } });
|
||||
}
|
||||
|
||||
Artwork::pointer Artwork::create(Session& session, ObjectPtr<Image> image)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
return session.getDboSession()->add(std::unique_ptr<Artwork>{ new Artwork{ image } });
|
||||
}
|
||||
|
||||
std::size_t Artwork::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<long>("SELECT COUNT(*) FROM artwork"));
|
||||
}
|
||||
|
||||
Artwork::pointer Artwork::find(Session& session, ArtworkId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.id = ?").bind(id));
|
||||
}
|
||||
|
||||
Artwork::pointer Artwork::find(Session& session, TrackEmbeddedImageId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.track_embedded_image_id = ?").bind(id));
|
||||
}
|
||||
|
||||
Artwork::pointer Artwork::find(Session& session, ImageId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.image_id = ?").bind(id));
|
||||
}
|
||||
|
||||
Wt::WDateTime Artwork::getLastWrittenTime() const
|
||||
{
|
||||
auto query{ session()->query<Wt::WDateTime>("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") };
|
||||
query.leftJoin("image ON artwork.image_id = image.id");
|
||||
query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id");
|
||||
query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id");
|
||||
query.leftJoin("track ON track.id = track_embedded_image_link.track_id");
|
||||
query.where("artwork.id = ?").bind(getId());
|
||||
|
||||
return utils::fetchQuerySingleResult(query);
|
||||
}
|
||||
} // namespace lms::db
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "database/Cluster.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "database/Db.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||
|
||||
@@ -70,21 +73,98 @@ namespace lms::db
|
||||
|
||||
std::filesystem::path _dbPath;
|
||||
};
|
||||
|
||||
enum class IntegrityCheckType
|
||||
{
|
||||
Quick,
|
||||
Full
|
||||
};
|
||||
bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function<void(std::string_view error)> errorCallback)
|
||||
{
|
||||
bool integrityCheckPassed{};
|
||||
|
||||
auto statement = connection.prepareStatement(checkType == IntegrityCheckType::Full ? "PRAGMA integrity_check" : "PRAGMA quick_check");
|
||||
statement->execute();
|
||||
|
||||
std::string result;
|
||||
result.reserve(32);
|
||||
while (statement->nextRow())
|
||||
{
|
||||
result.clear();
|
||||
statement->getResult(0, &result, result.capacity());
|
||||
|
||||
if (result == "ok")
|
||||
{
|
||||
integrityCheckPassed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
errorCallback(result);
|
||||
}
|
||||
|
||||
return integrityCheckPassed;
|
||||
}
|
||||
|
||||
bool checkDbForeignKeyConstraints(Wt::Dbo::SqlConnection& connection, std::function<void(std::string_view table, long long rowId, std::string_view referredTable)> errorCallback)
|
||||
{
|
||||
bool foreignKeyConstraintsPassed{ true };
|
||||
|
||||
auto statement = connection.prepareStatement("PRAGMA foreign_key_check");
|
||||
statement->execute();
|
||||
|
||||
std::string table;
|
||||
std::string foreignTable;
|
||||
// see https://www.sqlite.org/pragma.html#pragma_foreign_key_check for exepcted result
|
||||
while (statement->nextRow())
|
||||
{
|
||||
foreignKeyConstraintsPassed = false;
|
||||
|
||||
table.clear();
|
||||
foreignTable.clear();
|
||||
long long rowId{};
|
||||
|
||||
statement->getResult(0, &table, static_cast<int>(table.capacity()));
|
||||
statement->getResult(1, &rowId);
|
||||
statement->getResult(2, &foreignTable, static_cast<int>(foreignTable.capacity()));
|
||||
|
||||
errorCallback(table, rowId, foreignTable);
|
||||
}
|
||||
|
||||
return foreignKeyConstraintsPassed;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Session living class handling the database and the login
|
||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||
{
|
||||
std::string checkType{ "quick" };
|
||||
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath);
|
||||
|
||||
auto connection{ std::make_unique<Connection>(dbPath) };
|
||||
if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU
|
||||
{
|
||||
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
|
||||
checkType = config->getString("db-integrity-check", "quick");
|
||||
}
|
||||
|
||||
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
||||
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||
|
||||
_connectionPool = std::move(connectionPool);
|
||||
|
||||
if (checkType == "quick")
|
||||
{
|
||||
performQuickCheck();
|
||||
}
|
||||
else if (checkType == "full")
|
||||
{
|
||||
performIntegrityCheck();
|
||||
performForeignKeyConstraintsCheck();
|
||||
}
|
||||
else if (checkType != "none")
|
||||
{
|
||||
throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'.");
|
||||
}
|
||||
}
|
||||
|
||||
void Db::executeSql(const std::string& sql)
|
||||
@@ -114,6 +194,55 @@ namespace lms::db
|
||||
return *tlsSession;
|
||||
}
|
||||
|
||||
void Db::performQuickCheck()
|
||||
{
|
||||
ScopedConnection connection{ *_connectionPool };
|
||||
|
||||
LMS_LOG(DB, INFO, "Performing quick database check...");
|
||||
|
||||
// Quick check is just a simple integrity check
|
||||
bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) {
|
||||
LMS_LOG(DB, ERROR, "Quick check error: " << error);
|
||||
}) };
|
||||
|
||||
if (quickCheckPassed)
|
||||
LMS_LOG(DB, INFO, "Quick database check passed!");
|
||||
else
|
||||
LMS_LOG(DB, ERROR, "Quick database check done with errors!");
|
||||
}
|
||||
|
||||
void Db::performIntegrityCheck()
|
||||
{
|
||||
ScopedConnection connection{ *_connectionPool };
|
||||
|
||||
LMS_LOG(DB, INFO, "Checking database integrity...");
|
||||
|
||||
bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) {
|
||||
LMS_LOG(DB, ERROR, "Integrity check error: " << error);
|
||||
}) };
|
||||
|
||||
if (integrityCheckPassed)
|
||||
LMS_LOG(DB, INFO, "Database integrity check passed!");
|
||||
else
|
||||
LMS_LOG(DB, ERROR, "Database integrity check done with errors!");
|
||||
}
|
||||
|
||||
void Db::performForeignKeyConstraintsCheck()
|
||||
{
|
||||
ScopedConnection connection{ *_connectionPool };
|
||||
|
||||
LMS_LOG(DB, INFO, "Checking foreign key constraints...");
|
||||
|
||||
bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) {
|
||||
LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'");
|
||||
}) };
|
||||
|
||||
if (!foreignKeyConstraintsPassed)
|
||||
throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database.");
|
||||
|
||||
LMS_LOG(DB, INFO, "Foreign key constraints check passed!");
|
||||
}
|
||||
|
||||
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
|
||||
: _connectionPool{ pool }
|
||||
, _connection{ _connectionPool.getConnection() }
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 90 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 94 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -411,7 +411,7 @@ SELECT
|
||||
copyright_url,
|
||||
track_replay_gain,
|
||||
release_replay_gain,
|
||||
COALESCE(artist_display_name, ""),
|
||||
artist_display_name,
|
||||
release_id,
|
||||
1
|
||||
FROM track)");
|
||||
@@ -1206,6 +1206,186 @@ FROM tracklist)");
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV90(Session& session)
|
||||
{
|
||||
// Added a fallback for missing name, using parent directory name
|
||||
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV91(Session& session)
|
||||
{
|
||||
dropIndexes(session);
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred");
|
||||
}
|
||||
|
||||
void migrateFromV92(Session& session)
|
||||
{
|
||||
// Create the new artwork table
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artwork" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"track_embedded_image_id" bigint,
|
||||
"image_id" bigint,
|
||||
constraint "fk_artwork_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_artwork_image" foreign key ("image_id") references "image" ("id") on delete cascade deferrable initially deferred))");
|
||||
|
||||
// Replaced image by artwork for release
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"sort_name" text not null,
|
||||
"mbid" text not null,
|
||||
"group_mbid" text not null,
|
||||
"total_disc" integer,
|
||||
"artist_display_name" text not null,
|
||||
"is_compilation" boolean not null,
|
||||
"barcode" text not null,
|
||||
"comment" text not null,
|
||||
"preferred_artwork_id" bigint,
|
||||
constraint "fk_release_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))");
|
||||
// Migrate data, with the new preferred_artwork_id field set to null
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO release_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
name,
|
||||
sort_name,
|
||||
mbid,
|
||||
group_mbid,
|
||||
total_disc,
|
||||
COALESCE(artist_display_name, ''),
|
||||
is_compilation,
|
||||
barcode,
|
||||
comment,
|
||||
NULL as preferred_artwork_id
|
||||
FROM release)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE release");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release");
|
||||
|
||||
// Replaced image by artwork for track
|
||||
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,
|
||||
"disc_number" integer,
|
||||
"total_track" integer,
|
||||
"disc_subtitle" text not null,
|
||||
"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,
|
||||
"relative_file_path" text not null,
|
||||
"file_stem" text not null,
|
||||
"file_name" 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,
|
||||
"track_replay_gain" real,
|
||||
"release_replay_gain" real,
|
||||
"artist_display_name" text not null,
|
||||
"comment" text not null,
|
||||
"release_id" bigint,
|
||||
"media_library_id" bigint,
|
||||
"directory_id" bigint,
|
||||
"preferred_artwork_id" bigint,
|
||||
"preferred_media_artwork_id" bigint,
|
||||
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, with the new preferred_artwork_id and preferred_media_artwork_id fields set to null
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
scan_version,
|
||||
track_number,
|
||||
disc_number,
|
||||
total_track,
|
||||
disc_subtitle,
|
||||
name,
|
||||
duration,
|
||||
bitrate,
|
||||
bits_per_sample,
|
||||
channel_count,
|
||||
sample_rate,
|
||||
date,
|
||||
original_date,
|
||||
absolute_file_path,
|
||||
relative_file_path,
|
||||
file_stem,
|
||||
file_name,
|
||||
file_size,
|
||||
file_last_write,
|
||||
file_added,
|
||||
mbid,
|
||||
recording_mbid,
|
||||
copyright,
|
||||
copyright_url,
|
||||
advisory,
|
||||
track_replay_gain,
|
||||
release_replay_gain,
|
||||
artist_display_name,
|
||||
comment,
|
||||
release_id,
|
||||
media_library_id,
|
||||
directory_id,
|
||||
NULL as preferred_artwork_id,
|
||||
NULL as preferred_media_artwork_id
|
||||
FROM track)");
|
||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Replaced image by artwork for artist
|
||||
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"sort_name" text not null,
|
||||
"mbid" text not null,
|
||||
"preferred_artwork_id" bigint,
|
||||
constraint "fk_artist_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
|
||||
))");
|
||||
// Migrate data, with the new preferred_artwork_id field set to null
|
||||
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO artist_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
name,
|
||||
sort_name,
|
||||
mbid,
|
||||
NULL as preferred_artwork_id
|
||||
FROM artist)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DROP TABLE artist");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV93(Session& session)
|
||||
{
|
||||
// add artist_image_fallback_to_release in ScanSettings
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN artist_image_fallback_to_release BOOLEAN NOT NULL DEFAULT(false)");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1272,6 +1452,10 @@ FROM tracklist)");
|
||||
{ 87, migrateFromV87 },
|
||||
{ 88, migrateFromV88 },
|
||||
{ 89, migrateFromV89 },
|
||||
{ 90, migrateFromV90 },
|
||||
{ 91, migrateFromV91 },
|
||||
{ 92, migrateFromV92 },
|
||||
{ 93, migrateFromV93 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <Wt/Dbo/SqlTraits.h>
|
||||
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/Types.hpp"
|
||||
@@ -701,9 +701,14 @@ namespace lms::db
|
||||
return utils::fetchQueryResults<Release::pointer>(query);
|
||||
}
|
||||
|
||||
ObjectPtr<Image> Release::getImage() const
|
||||
ObjectPtr<Artwork> Release::getPreferredArtwork() const
|
||||
{
|
||||
return ObjectPtr<Image>{ _image };
|
||||
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||
}
|
||||
|
||||
ArtworkId Release::getPreferredArtworkId() const
|
||||
{
|
||||
return _preferredArtwork.id();
|
||||
}
|
||||
|
||||
void Release::clearLabels()
|
||||
@@ -736,9 +741,9 @@ namespace lms::db
|
||||
_releaseTypes.insert(getDboPtr(releaseType));
|
||||
}
|
||||
|
||||
void Release::setImage(ObjectPtr<Image> image)
|
||||
void Release::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||
{
|
||||
_image = getDboPtr(image);
|
||||
_preferredArtwork = getDboPtr(artwork);
|
||||
}
|
||||
|
||||
bool Release::hasVariousArtists() const
|
||||
@@ -867,14 +872,11 @@ namespace lms::db
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
utils::forEachQueryResult(query, [&](const Wt::Dbo::ptr<Cluster>& cluster) {
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
@@ -882,4 +884,23 @@ namespace lms::db
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<ObjectPtr<Cluster>> Release::getClusters(ClusterTypeId clusterTypeId, std::size_t maxCount) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") };
|
||||
query.where("t.release_id = ?").bind(getId());
|
||||
query.where("c.cluster_type_id = ?").bind(clusterTypeId);
|
||||
query.groupBy("c.id");
|
||||
query.orderBy("COUNT(c.id) DESC");
|
||||
query.limit(static_cast<int>(maxCount));
|
||||
|
||||
std::vector<ObjectPtr<Cluster>> res;
|
||||
utils::forEachQueryResult(query, [&](const Wt::Dbo::ptr<Cluster>& cluster) {
|
||||
res.push_back(cluster);
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -121,14 +121,17 @@ namespace lms::db
|
||||
|
||||
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
|
||||
{
|
||||
if (_skipSingleReleasePlayLists != value)
|
||||
_skipSingleReleasePlayLists = value;
|
||||
_skipSingleReleasePlayLists = value;
|
||||
}
|
||||
|
||||
void ScanSettings::setAllowMBIDArtistMerge(bool value)
|
||||
{
|
||||
if (_allowMBIDArtistMerge != value)
|
||||
_allowMBIDArtistMerge = value;
|
||||
_allowMBIDArtistMerge = value;
|
||||
}
|
||||
|
||||
void ScanSettings::setArtistImageFallbackToReleaseField(bool value)
|
||||
{
|
||||
_artistImageFallbackToReleaseField = value;
|
||||
}
|
||||
|
||||
void ScanSettings::incAudioScanVersion()
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/ArtistInfo.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/AuthToken.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
@@ -102,6 +103,7 @@ namespace lms::db
|
||||
|
||||
_session.mapClass<Artist>("artist");
|
||||
_session.mapClass<ArtistInfo>("artist_info");
|
||||
_session.mapClass<Artwork>("artwork");
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
@@ -196,7 +198,6 @@ namespace lms::db
|
||||
{
|
||||
auto transaction{ createWriteTransaction() };
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||
@@ -206,10 +207,15 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_id_idx ON cluster(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
|
||||
@@ -225,9 +231,10 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_type_track_embedded_image_id_track_id_idx ON track_embedded_image_link(type,track_embedded_image_id,track_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
||||
@@ -254,7 +261,6 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||
@@ -291,6 +297,7 @@ namespace lms::db
|
||||
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_desc_idx ON track(release_id, file_added DESC)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_id_idx ON tracklist(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_user_type_idx ON tracklist(user_id, type)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_last_modified_date_time_idx ON tracklist(last_modified_date_time)");
|
||||
@@ -298,6 +305,7 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_idx ON tracklist_entry(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_id_idx ON track_artist_link(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_mbid_matched_artist_idx ON track_artist_link(artist_mbid_matched, artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
||||
@@ -393,6 +401,16 @@ namespace lms::db
|
||||
LMS_LOG(DB, INFO, "Analyze complete!");
|
||||
}
|
||||
|
||||
bool Session::areAllTablesEmpty()
|
||||
{
|
||||
const std::vector<std::string> entryList{ utils::fetchQueryResults(_session.query<std::string>("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")) };
|
||||
|
||||
return std::all_of(entryList.cbegin(), entryList.cend(), [this](const std::string& entry) {
|
||||
const auto count{ utils::fetchQuerySingleResult(_session.query<long>("SELECT COUNT(*) FROM " + entry)) };
|
||||
return count == 0;
|
||||
});
|
||||
}
|
||||
|
||||
void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList)
|
||||
{
|
||||
auto transaction{ createReadTransaction() };
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
@@ -324,6 +325,26 @@ namespace lms::db
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
if (artworkId.isValid())
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = ? WHERE id = ?", artworkId, trackId);
|
||||
else
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = NULL WHERE id = ?", trackId);
|
||||
}
|
||||
|
||||
void Track::updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
if (artworkId.isValid())
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = ? WHERE id = ?", artworkId, trackId);
|
||||
else
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = NULL WHERE id = ?", trackId);
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> Track::getClusters() const
|
||||
{
|
||||
return utils::fetchQueryResults<Cluster::pointer>(_clusters.find());
|
||||
@@ -338,6 +359,36 @@ namespace lms::db
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
ObjectPtr<MediaLibrary> Track::getMediaLibrary() const
|
||||
{
|
||||
return _mediaLibrary;
|
||||
}
|
||||
|
||||
ObjectPtr<Directory> Track::getDirectory() const
|
||||
{
|
||||
return _directory;
|
||||
}
|
||||
|
||||
ObjectPtr<Artwork> Track::getPreferredArtwork() const
|
||||
{
|
||||
return _preferredArtwork;
|
||||
}
|
||||
|
||||
ArtworkId Track::getPreferredArtworkId() const
|
||||
{
|
||||
return _preferredArtwork.id();
|
||||
}
|
||||
|
||||
ObjectPtr<Artwork> Track::getPreferredMediaArtwork() const
|
||||
{
|
||||
return _preferredMediaArtwork;
|
||||
}
|
||||
|
||||
ArtworkId Track::getPreferredMediaArtworkId() const
|
||||
{
|
||||
return _preferredMediaArtwork.id();
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
@@ -484,6 +535,26 @@ namespace lms::db
|
||||
_embeddedImageLinks.insert(getDboPtr(image));
|
||||
}
|
||||
|
||||
void Track::setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary)
|
||||
{
|
||||
_mediaLibrary = getDboPtr(mediaLibrary);
|
||||
}
|
||||
|
||||
void Track::setDirectory(ObjectPtr<Directory> directory)
|
||||
{
|
||||
_directory = getDboPtr(directory);
|
||||
}
|
||||
|
||||
void Track::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||
{
|
||||
_preferredArtwork = getDboPtr(artwork);
|
||||
}
|
||||
|
||||
void Track::setPreferredMediaArtwork(ObjectPtr<Artwork> artwork)
|
||||
{
|
||||
_preferredMediaArtwork = getDboPtr(artwork);
|
||||
}
|
||||
|
||||
std::optional<int> Track::getYear() const
|
||||
{
|
||||
return _date.getYear();
|
||||
|
||||
@@ -38,24 +38,50 @@ namespace lms::db
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
|
||||
|
||||
if (params.isPreferred
|
||||
if (params.artist.isValid()
|
||||
|| params.discNumber.has_value()
|
||||
|| params.track.isValid()
|
||||
|| params.release.isValid()
|
||||
|| params.trackList.isValid()
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize)
|
||||
|| !params.imageTypes.empty()
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
|
||||
{
|
||||
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.isPreferred)
|
||||
query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value());
|
||||
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())
|
||||
query.where("t_e_i_l.track_id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid())
|
||||
if (params.release.isValid()
|
||||
|| params.discNumber.has_value()
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|
||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
|
||||
{
|
||||
query.join("track t ON t_e_i_l.track_id = t.id");
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
if (params.discNumber.has_value())
|
||||
query.where("t.disc_number = ?").bind(params.discNumber.value());
|
||||
}
|
||||
|
||||
if (params.trackList.isValid())
|
||||
@@ -63,14 +89,38 @@ namespace lms::db
|
||||
query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id");
|
||||
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
|
||||
}
|
||||
|
||||
if (!params.imageTypes.empty())
|
||||
{
|
||||
std::string clause{ "t_e_i_l.type IN (" };
|
||||
for (const auto& type : params.imageTypes)
|
||||
{
|
||||
if (clause.back() != '(')
|
||||
clause += ",";
|
||||
clause += "?";
|
||||
query.bind(type);
|
||||
}
|
||||
clause += ")";
|
||||
query.where(clause);
|
||||
}
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case TrackEmbeddedImageSortMethod::None:
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::FrontCoverAndSize:
|
||||
query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover);
|
||||
case TrackEmbeddedImageSortMethod::SizeDesc:
|
||||
query.orderBy("t_e_i.size DESC");
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
|
||||
query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC");
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
|
||||
query.orderBy("t.track_number, t_e_i.size DESC");
|
||||
break;
|
||||
case TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc:
|
||||
assert(params.trackList.isValid());
|
||||
query.orderBy("t_l_e.id, t_e_i.size DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,14 @@ namespace lms::db
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImageLink>().where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
void TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function<void(const pointer&)> visitor)
|
||||
{
|
||||
auto query{ session.getDboSession()->find<TrackEmbeddedImageLink>() };
|
||||
query.where("track_embedded_image_id = ?").bind(trackEmbeddedImageId);
|
||||
|
||||
return utils::forEachQueryResult(query, visitor);
|
||||
}
|
||||
|
||||
ObjectPtr<Track> TrackEmbeddedImageLink::getTrack() const
|
||||
{
|
||||
return _track;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/Filters.hpp"
|
||||
#include "database/MediaLibraryId.hpp"
|
||||
@@ -42,10 +43,9 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
class Artwork;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Image;
|
||||
class Release;
|
||||
class Session;
|
||||
class StarredArtist;
|
||||
@@ -136,7 +136,8 @@ namespace lms::db
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<core::UUID> getMBID() const;
|
||||
bool hasMBID() const;
|
||||
ObjectPtr<Image> getImage() const;
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
ArtworkId getPreferredArtworkId() const;
|
||||
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
@@ -150,7 +151,7 @@ namespace lms::db
|
||||
void setName(std::string_view name);
|
||||
void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(std::string_view sortName);
|
||||
void setImage(ObjectPtr<Image> image);
|
||||
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -159,7 +160,7 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _mbid, "mbid");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
@@ -176,7 +177,7 @@ namespace lms::db
|
||||
std::string _sortName;
|
||||
std::string _mbid; // Musicbrainz Identifier
|
||||
|
||||
Wt::Dbo::ptr<Image> _image;
|
||||
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Image;
|
||||
class Session;
|
||||
class TrackEmbeddedImage;
|
||||
|
||||
class Artwork final : public Object<Artwork, ArtworkId>
|
||||
{
|
||||
public:
|
||||
Artwork() = default;
|
||||
|
||||
// find
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, ArtworkId id);
|
||||
static pointer find(Session& session, TrackEmbeddedImageId id);
|
||||
static pointer find(Session& session, ImageId id);
|
||||
|
||||
// getters
|
||||
TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); }
|
||||
ImageId getImageId() const { return _image.id(); }
|
||||
Wt::WDateTime getLastWrittenTime() const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::belongsTo(a, _trackEmbeddedImage, "track_embedded_image", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Artwork(ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage);
|
||||
Artwork(ObjectPtr<Image> image);
|
||||
static pointer create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage);
|
||||
static pointer create(Session& session, ObjectPtr<Image> image);
|
||||
|
||||
Wt::Dbo::ptr<TrackEmbeddedImage> _trackEmbeddedImage;
|
||||
Wt::Dbo::ptr<Image> _image;
|
||||
};
|
||||
} // namespace lms::db
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
LMS_DECLARE_IDTYPE(ArtworkId)
|
||||
@@ -46,6 +46,10 @@ namespace lms::db
|
||||
core::RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||
|
||||
void performQuickCheck();
|
||||
void performIntegrityCheck();
|
||||
void performForeignKeyConstraintsCheck();
|
||||
|
||||
class ScopedConnection
|
||||
{
|
||||
public:
|
||||
@@ -53,6 +57,7 @@ namespace lms::db
|
||||
~ScopedConnection();
|
||||
|
||||
Wt::Dbo::SqlConnection* operator->() const;
|
||||
Wt::Dbo::SqlConnection& operator*() const { return *_connection; }
|
||||
|
||||
private:
|
||||
ScopedConnection(const ScopedConnection&) = delete;
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Artist;
|
||||
class Directory;
|
||||
class Release;
|
||||
class Session;
|
||||
|
||||
class Image final : public Object<Image, ImageId>
|
||||
@@ -100,8 +98,6 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _width, "width");
|
||||
Wt::Dbo::field(a, _height, "height");
|
||||
|
||||
Wt::Dbo::hasMany(a, _artists, Wt::Dbo::ManyToOne, "image");
|
||||
Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToOne, "image");
|
||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
@@ -117,8 +113,6 @@ namespace lms::db
|
||||
int _width{};
|
||||
int _height{};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _artists;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases;
|
||||
Wt::Dbo::ptr<Directory> _directory;
|
||||
};
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "database/CountryId.hpp"
|
||||
#include "database/DirectoryId.hpp"
|
||||
#include "database/Filters.hpp"
|
||||
@@ -45,9 +46,9 @@
|
||||
namespace lms::db
|
||||
{
|
||||
class Artist;
|
||||
class Artwork;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Image;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
@@ -258,6 +259,7 @@ namespace lms::db
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
std::vector<ObjectPtr<Cluster>> getClusters(ClusterTypeId clusterTypeId, std::size_t maxCount) const;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
core::PartialDateTime getDate() const;
|
||||
@@ -290,7 +292,8 @@ namespace lms::db
|
||||
core::EnumSet<Advisory> getAdvisories() const;
|
||||
std::string_view getBarcode() const { return _barcode; }
|
||||
std::string_view getComment() const { return _comment; }
|
||||
ObjectPtr<Image> getImage() const;
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
ArtworkId getPreferredArtworkId() const;
|
||||
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
@@ -308,7 +311,7 @@ namespace lms::db
|
||||
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
|
||||
void setBarcode(std::string_view barcode) { _barcode = barcode; }
|
||||
void setComment(std::string_view comment) { _comment = comment; }
|
||||
void setImage(ObjectPtr<Image> image);
|
||||
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
@@ -330,9 +333,9 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _isCompilation, "is_compilation");
|
||||
Wt::Dbo::field(a, _barcode, "barcode");
|
||||
Wt::Dbo::field(a, _comment, "comment");
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -358,8 +361,8 @@ namespace lms::db
|
||||
std::string _barcode;
|
||||
std::string _comment;
|
||||
|
||||
Wt::Dbo::ptr<Image> _image;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Country>> _countries;
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace lms::db
|
||||
std::vector<std::string> getArtistsToNotSplit() const;
|
||||
bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; }
|
||||
bool getAllowMBIDArtistMerge() const { return _allowMBIDArtistMerge; }
|
||||
bool getArtistImageFallbackToReleaseField() const { return _artistImageFallbackToReleaseField; }
|
||||
|
||||
// Setters
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
@@ -85,6 +86,7 @@ namespace lms::db
|
||||
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
||||
void setSkipSingleReleasePlayLists(bool value);
|
||||
void setAllowMBIDArtistMerge(bool value);
|
||||
void setArtistImageFallbackToReleaseField(bool value);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -101,6 +103,7 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters");
|
||||
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
||||
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
||||
Wt::Dbo::field(a, _artistImageFallbackToReleaseField, "artist_image_fallback_to_release");
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -123,5 +126,6 @@ namespace lms::db
|
||||
std::string _defaultTagDelimiters;
|
||||
bool _skipSingleReleasePlayLists{};
|
||||
bool _allowMBIDArtistMerge{};
|
||||
bool _artistImageFallbackToReleaseField{};
|
||||
};
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -96,6 +96,8 @@ namespace lms::db
|
||||
void retrieveEntriesToAnalyze(std::vector<std::string>& entryList);
|
||||
void analyzeEntry(const std::string& entry);
|
||||
|
||||
bool areAllTablesEmpty(); // need to acquire a read transaction
|
||||
|
||||
void prepareTablesIfNeeded(); // need to run only once at startup
|
||||
bool migrateSchemaIfNeeded(); // returns true if migration was performed
|
||||
void createIndexesIfNeeded();
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/UUID.hpp"
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/DirectoryId.hpp"
|
||||
#include "database/Filters.hpp"
|
||||
@@ -50,6 +51,7 @@
|
||||
namespace lms::db
|
||||
{
|
||||
class Artist;
|
||||
class Artwork;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Directory;
|
||||
@@ -215,6 +217,10 @@ namespace lms::db
|
||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Update utility functions
|
||||
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||
static void updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||
|
||||
// Accessors
|
||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||
@@ -252,8 +258,10 @@ namespace lms::db
|
||||
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
||||
void clearEmbeddedImageLinks();
|
||||
void addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& link);
|
||||
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
|
||||
void setDirectory(ObjectPtr<Directory> directory) { _directory = getDboPtr(directory); }
|
||||
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary);
|
||||
void setDirectory(ObjectPtr<Directory> directory);
|
||||
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||
void setPreferredMediaArtwork(ObjectPtr<Artwork> artwork);
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||
@@ -295,8 +303,12 @@ namespace lms::db
|
||||
ObjectPtr<Release> getRelease() const { return _release; }
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; }
|
||||
ObjectPtr<Directory> getDirectory() const { return _directory; }
|
||||
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
||||
ObjectPtr<Directory> getDirectory() const;
|
||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||
ArtworkId getPreferredArtworkId() const;
|
||||
ObjectPtr<Artwork> getPreferredMediaArtwork() const;
|
||||
ArtworkId getPreferredMediaArtworkId() const;
|
||||
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
||||
|
||||
@@ -331,11 +343,13 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_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, _comment, "comment");
|
||||
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
|
||||
|
||||
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, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
||||
@@ -382,6 +396,8 @@ namespace lms::db
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||
Wt::Dbo::ptr<Directory> _directory;
|
||||
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||
Wt::Dbo::ptr<Artwork> _preferredMediaArtwork;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
@@ -46,10 +48,13 @@ namespace lms::db
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ArtistId artist;
|
||||
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes;
|
||||
TrackId track;
|
||||
ReleaseId release;
|
||||
std::optional<int> discNumber;
|
||||
TrackListId trackList;
|
||||
std::optional<bool> isPreferred;
|
||||
core::EnumSet<ImageType> imageTypes;
|
||||
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range)
|
||||
@@ -57,6 +62,13 @@ namespace lms::db
|
||||
range = _range;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
|
||||
FindParameters& setTrack(TrackId _track)
|
||||
{
|
||||
track = _track;
|
||||
@@ -67,14 +79,19 @@ namespace lms::db
|
||||
release = _release;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setDiscNumber(std::optional<int> _discNumber)
|
||||
{
|
||||
discNumber = _discNumber;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setTrackList(TrackListId _trackList)
|
||||
{
|
||||
trackList = _trackList;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setIsPreferred(std::optional<bool> _isPreferred)
|
||||
FindParameters& setImageTypes(core::EnumSet<ImageType> _imageTypes)
|
||||
{
|
||||
isPreferred = _isPreferred;
|
||||
imageTypes = _imageTypes;
|
||||
return *this;
|
||||
}
|
||||
FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/Object.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
#include "database/TrackEmbeddedImageLinkId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
@@ -44,18 +45,17 @@ namespace lms::db
|
||||
// find
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, TrackEmbeddedImageLinkId id);
|
||||
static void find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function<void(const pointer&)> visitor);
|
||||
|
||||
// getters
|
||||
ObjectPtr<Track> getTrack() const;
|
||||
ObjectPtr<TrackEmbeddedImage> getImage() const;
|
||||
std::size_t getIndex() const { return _index; }
|
||||
bool isPreferred() const { return _isPreferred; }
|
||||
ImageType getType() const { return _type; }
|
||||
std::string_view getDescription() const { return _description; }
|
||||
|
||||
// setters
|
||||
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
|
||||
void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; }
|
||||
void setType(ImageType type) { _type = type; }
|
||||
void setDescription(std::string_view description) { _description = description; }
|
||||
|
||||
@@ -63,7 +63,6 @@ namespace lms::db
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _index, "index");
|
||||
Wt::Dbo::field(a, _isPreferred, "is_preferred");
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _description, "description");
|
||||
|
||||
@@ -78,7 +77,6 @@ namespace lms::db
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
|
||||
|
||||
int _index{}; // index within the track
|
||||
bool _isPreferred{};
|
||||
ImageType _type{ ImageType::Unknown };
|
||||
std::string _description;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -171,7 +172,10 @@ namespace lms::db
|
||||
enum class TrackEmbeddedImageSortMethod
|
||||
{
|
||||
None,
|
||||
FrontCoverAndSize,
|
||||
SizeDesc,
|
||||
TrackNumberThenSizeDesc,
|
||||
DiscNumberThenTrackNumberThenSizeDesc,
|
||||
TrackListIndexAscThenSizeDesc,
|
||||
};
|
||||
|
||||
enum class TrackListSortMethod
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Image.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
|
||||
TEST_F(DatabaseFixture, Artist)
|
||||
@@ -711,27 +713,28 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_image)
|
||||
TEST_F(DatabaseFixture, Artist_artwork)
|
||||
{
|
||||
ScopedArtist release{ session, "MyArtist" };
|
||||
ScopedImage image{ session, "/image1.jpg" };
|
||||
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_FALSE(release.get()->getImage());
|
||||
EXPECT_FALSE(artist.get()->getPreferredArtwork());
|
||||
}
|
||||
|
||||
ScopedImage image{ session, "/myImage" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
release.get().modify()->setImage(image.get());
|
||||
artist.get().modify()->setPreferredArtwork(artwork.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto artistImage(release.get()->getImage());
|
||||
ASSERT_TRUE(artistImage);
|
||||
EXPECT_EQ(artistImage->getId(), image.getId());
|
||||
auto artistArtwork(artist.get()->getPreferredArtwork());
|
||||
ASSERT_TRUE(artistArtwork);
|
||||
EXPECT_EQ(artistArtwork->getId(), artwork.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2025of 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/Artwork.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
|
||||
|
||||
TEST_F(DatabaseFixture, Artwork_image)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Artwork::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedImage image{ session, "/MyImage" };
|
||||
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Artwork::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
image.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artwork_trackEmbeddedImage)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Artwork::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackEmbeddedImage image{ session };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<db::TrackEmbeddedImageLink>(track.get(), image.get());
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -2,6 +2,7 @@
|
||||
add_executable(test-database
|
||||
Artist.cpp
|
||||
ArtistInfo.cpp
|
||||
Artwork.cpp
|
||||
AuthToken.cpp
|
||||
Cluster.cpp
|
||||
Common.cpp
|
||||
|
||||
@@ -19,23 +19,10 @@
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/Listen.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
@@ -75,24 +62,9 @@ namespace lms::db::tests
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Artist::getCount(session), 0);
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||
EXPECT_EQ(Label::getCount(session), 0);
|
||||
EXPECT_EQ(Listen::getCount(session), 0);
|
||||
EXPECT_EQ(Image::getCount(session), 0);
|
||||
EXPECT_EQ(MediaLibrary::getCount(session), 0);
|
||||
EXPECT_EQ(Release::getCount(session), 0);
|
||||
EXPECT_EQ(ReleaseType::getCount(session), 0);
|
||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
EXPECT_EQ(Track::getCount(session), 0);
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||
EXPECT_EQ(User::getCount(session), 0);
|
||||
EXPECT_TRUE(session.areAllTablesEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, vacuum)
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Image.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
using ScopedLabel = ScopedEntity<db::Label>;
|
||||
using ScopedCountry = ScopedEntity<db::Country>;
|
||||
@@ -1195,27 +1197,40 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_image)
|
||||
TEST_F(DatabaseFixture, Release_artwork)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_FALSE(release.get()->getImage());
|
||||
EXPECT_FALSE(release.get()->getPreferredArtwork());
|
||||
}
|
||||
|
||||
ScopedImage image{ session, "/myImage" };
|
||||
ScopedImage image{ session, "/image.jpg" };
|
||||
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
release.get().modify()->setImage(image.get());
|
||||
release.get().modify()->setPreferredArtwork(artwork.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto releaseImage(release.get()->getImage());
|
||||
ASSERT_TRUE(releaseImage);
|
||||
EXPECT_EQ(releaseImage->getId(), image.getId());
|
||||
auto releaseArtwork(release.get()->getPreferredArtwork());
|
||||
ASSERT_TRUE(releaseArtwork);
|
||||
EXPECT_EQ(releaseArtwork->getId(), artwork.getId());
|
||||
}
|
||||
|
||||
// Check cascade delete
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
image.lockAndGet().remove();
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto releaseArtwork(release.get()->getPreferredArtwork());
|
||||
ASSERT_FALSE(releaseArtwork);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Image.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||
using ScopedImage = ScopedEntity<db::Image>;
|
||||
|
||||
TEST_F(DatabaseFixture, Track)
|
||||
{
|
||||
{
|
||||
@@ -439,4 +445,41 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(tracks.results[3], track3.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_updateArtworks)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{});
|
||||
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
|
||||
}
|
||||
|
||||
ScopedImage image1{ session, "/image1.jpg" };
|
||||
ScopedArtwork artwork1{ session, image1.lockAndGet() };
|
||||
ScopedImage image2{ session, "/image2.jpg" };
|
||||
ScopedArtwork artwork2{ session, image2.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
Track::updatePreferredArtwork(session, track->getId(), artwork1->getId());
|
||||
Track::updatePreferredMediaArtwork(session, track->getId(), artwork2->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getPreferredArtwork()->getId(), artwork1->getId());
|
||||
EXPECT_EQ(track->getPreferredMediaArtwork()->getId(), artwork2->getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
Track::updatePreferredArtwork(session, track->getId(), ArtworkId{});
|
||||
Track::updatePreferredMediaArtwork(session, track->getId(), ArtworkId{});
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{});
|
||||
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "Common.hpp"
|
||||
@@ -118,23 +119,6 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setIsPreferred(true);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
EXPECT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
link.get().modify()->setIsPreferred(true);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setIsPreferred(true);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
@@ -142,15 +126,8 @@ namespace lms::db::tests
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setIsPreferred(true);
|
||||
params.setRelease(release.getId());
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
EXPECT_FALSE(visited);
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
link.get().modify()->setType(ImageType::FrontCover);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -162,9 +139,8 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setIsPreferred(true);
|
||||
params.setRelease(release.getId());
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
@@ -175,9 +151,8 @@ namespace lms::db::tests
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setIsPreferred(true);
|
||||
params.setTrack(track.getId());
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||
@@ -185,6 +160,159 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts)
|
||||
{
|
||||
ScopedTrackEmbeddedImage image1{ session };
|
||||
ScopedTrackEmbeddedImage image2{ session };
|
||||
ScopedTrackEmbeddedImage image3{ session };
|
||||
ScopedTrackEmbeddedImage image4{ session };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() };
|
||||
ScopedTrackEmbeddedImageLink link4{ session, track2.lockAndGet(), image4.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
track1.get().modify()->setTrackNumber(2);
|
||||
|
||||
link1.get().modify()->setType(ImageType::FrontCover);
|
||||
image1.get().modify()->setSize(750);
|
||||
link2.get().modify()->setType(ImageType::Media);
|
||||
image2.get().modify()->setSize(1000);
|
||||
link3.get().modify()->setType(ImageType::Media);
|
||||
image3.get().modify()->setSize(2000);
|
||||
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setTrackNumber(1);
|
||||
|
||||
link4.get().modify()->setType(ImageType::Media);
|
||||
image4.get().modify()->setSize(1500);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setRelease(release.getId());
|
||||
params.setImageTypes({ ImageType::Media });
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::SizeDesc);
|
||||
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||
ASSERT_EQ(visitedIds.size(), 3);
|
||||
EXPECT_EQ(visitedIds[0], image3.getId());
|
||||
EXPECT_EQ(visitedIds[1], image4.getId());
|
||||
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setRelease(release.getId());
|
||||
params.setImageTypes({ ImageType::Media });
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||
ASSERT_EQ(visitedIds.size(), 3);
|
||||
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setRelease(release.getId());
|
||||
params.setImageTypes({ ImageType::Media });
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||
ASSERT_EQ(visitedIds.size(), 3);
|
||||
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist)
|
||||
{
|
||||
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 };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setDiscNumber(1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setDiscNumber(1);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setDiscNumber(2);
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Track_findByEmbeddedImage)
|
||||
{
|
||||
ScopedTrackEmbeddedImage image{ session };
|
||||
@@ -279,5 +407,54 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(img->getType(), ImageType::FrontCover);
|
||||
EXPECT_EQ(img->getDescription(), "MyDesc");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackEmbeddedImageLink::find(session, image->getId(), [&](const TrackEmbeddedImageLink::pointer& link) {
|
||||
EXPECT_EQ(link->getIndex(), 2);
|
||||
EXPECT_EQ(link->getType(), ImageType::FrontCover);
|
||||
EXPECT_EQ(link->getDescription(), "MyDesc");
|
||||
EXPECT_EQ(link->getTrack(), track.get());
|
||||
|
||||
visited = true;
|
||||
});
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackEmbeddedImage_TrackList)
|
||||
{
|
||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::PlayList };
|
||||
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrackEmbeddedImage image1{ session };
|
||||
ScopedTrackEmbeddedImageLink imageLink1{ session, track1.lockAndGet(), image1.lockAndGet() };
|
||||
ScopedTrackEmbeddedImage image2{ session };
|
||||
ScopedTrackEmbeddedImageLink imageLink2{ session, track2.lockAndGet(), image2.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackEmbeddedImage::FindParameters params;
|
||||
params.setTrackList(trackList.getId());
|
||||
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc);
|
||||
|
||||
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) {
|
||||
visitedIds.push_back(image->getId());
|
||||
});
|
||||
ASSERT_EQ(visitedIds.size(), 2);
|
||||
EXPECT_EQ(visitedIds[0], image2.getId());
|
||||
EXPECT_EQ(visitedIds[1], image1.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -11,12 +11,14 @@ endif()
|
||||
add_library(lmsmetadata STATIC
|
||||
impl/ArtistInfo.cpp
|
||||
impl/AudioFileParser.cpp
|
||||
impl/AvFormatImageReader.cpp
|
||||
impl/AvFormatTagReader.cpp
|
||||
impl/avformat/AvFormatImageReader.cpp
|
||||
impl/avformat/AvFormatTagReader.cpp
|
||||
impl/avformat/Utils.cpp
|
||||
impl/Lyrics.cpp
|
||||
impl/PlayList.cpp
|
||||
impl/TagLibImageReader.cpp
|
||||
impl/TagLibTagReader.cpp
|
||||
impl/taglib/TagLibImageReader.cpp
|
||||
impl/taglib/TagLibTagReader.cpp
|
||||
impl/taglib/Utils.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
@@ -26,6 +28,7 @@ target_include_directories(lmsmetadata INTERFACE
|
||||
|
||||
target_include_directories(lmsmetadata PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsmetadata PRIVATE
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedInfoFileExtensions()
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles()
|
||||
{
|
||||
static const std::array<std::filesystem::path, 1> fileExtensions{ ".nfo" };
|
||||
return fileExtensions;
|
||||
static const std::array<std::filesystem::path, 1> files{ "artist.nfo" };
|
||||
return files;
|
||||
}
|
||||
|
||||
ArtistInfo parseArtistInfo(std::istream& is)
|
||||
@@ -56,7 +56,7 @@ namespace lms::metadata
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read artist xml info: " << error.what());
|
||||
LMS_LOG(METADATA, ERROR, "Cannot read artist xml info: " << error.what());
|
||||
throw ArtistInfoParseException{ error.what() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,13 @@
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "AvFormatImageReader.hpp"
|
||||
#include "AvFormatTagReader.hpp"
|
||||
#include "TagLibImageReader.hpp"
|
||||
#include "TagLibTagReader.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "avformat/AvFormatImageReader.hpp"
|
||||
#include "avformat/AvFormatTagReader.hpp"
|
||||
#include "avformat/Utils.hpp"
|
||||
#include "taglib/TagLibImageReader.hpp"
|
||||
#include "taglib/TagLibTagReader.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
@@ -338,82 +340,56 @@ namespace lms::metadata
|
||||
|
||||
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
|
||||
{
|
||||
// TODO: use backend capability to retrieve supported formats
|
||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||
".aac",
|
||||
".alac",
|
||||
".aif",
|
||||
".aiff",
|
||||
".ape",
|
||||
".dsf",
|
||||
".flac",
|
||||
".m4a",
|
||||
".m4b",
|
||||
".mp3",
|
||||
".mpc",
|
||||
".oga",
|
||||
".ogg",
|
||||
".opus",
|
||||
".shn",
|
||||
".wav",
|
||||
".wma",
|
||||
".wv",
|
||||
};
|
||||
return fileExtensions;
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
return taglib::utils::getSupportedExtensions();
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
return avformat::utils::getSupportedExtensions();
|
||||
break;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p) const
|
||||
{
|
||||
try
|
||||
std::unique_ptr<ITagReader> tagReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
std::unique_ptr<ITagReader> tagReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
tagReader = std::make_unique<TagLibTagReader>(p, _params.readStyle, _params.debug);
|
||||
break;
|
||||
case ParserBackend::TagLib:
|
||||
tagReader = std::make_unique<taglib::TagLibTagReader>(p, _params.readStyle, _params.debug);
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
tagReader = std::make_unique<AvFormatTagReader>(p, _params.debug);
|
||||
break;
|
||||
}
|
||||
if (!tagReader)
|
||||
throw ParseException{ "Unhandled parser backend" };
|
||||
case ParserBackend::AvFormat:
|
||||
tagReader = std::make_unique<avformat::AvFormatTagReader>(p, _params.debug);
|
||||
break;
|
||||
}
|
||||
if (!tagReader)
|
||||
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||
|
||||
return parseMetaData(*tagReader);
|
||||
}
|
||||
catch (const Exception& e)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed");
|
||||
throw ParseException{};
|
||||
}
|
||||
return parseMetaData(*tagReader);
|
||||
}
|
||||
|
||||
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor) const
|
||||
{
|
||||
try
|
||||
std::unique_ptr<IImageReader> imageReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
std::unique_ptr<IImageReader> imageReader;
|
||||
switch (_params.backend)
|
||||
{
|
||||
case ParserBackend::TagLib:
|
||||
imageReader = std::make_unique<TagLibImageReader>(p);
|
||||
break;
|
||||
case ParserBackend::TagLib:
|
||||
imageReader = std::make_unique<taglib::TagLibImageReader>(p);
|
||||
break;
|
||||
|
||||
case ParserBackend::AvFormat:
|
||||
imageReader = std::make_unique<AvFormatImageReader>(p);
|
||||
break;
|
||||
}
|
||||
if (!imageReader)
|
||||
throw ParseException{ "Unhandled parser backend" };
|
||||
case ParserBackend::AvFormat:
|
||||
imageReader = std::make_unique<avformat::AvFormatImageReader>(p);
|
||||
break;
|
||||
}
|
||||
if (!imageReader)
|
||||
throw AudioFileParsingException{ "Unhandled parser backend" };
|
||||
|
||||
parseImages(*imageReader, std::move(visitor));
|
||||
}
|
||||
catch (const Exception& e)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": image parsing failed");
|
||||
throw ParseException{};
|
||||
}
|
||||
parseImages(*imageReader, std::move(visitor));
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader) const
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace lms::metadata::utils
|
||||
return "accurate";
|
||||
}
|
||||
|
||||
throw core::LmsException{ "Unknown read style" };
|
||||
throw Exception{ "Unknown read style" };
|
||||
}
|
||||
|
||||
PerformerArtist extractPerformerAndRole(std::string_view entry)
|
||||
|
||||
+7
-5
@@ -18,10 +18,12 @@
|
||||
*/
|
||||
|
||||
#include "AvFormatImageReader.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "av/Types.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
#include "av/Exception.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
namespace lms::metadata::avformat
|
||||
{
|
||||
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
|
||||
{
|
||||
@@ -31,7 +33,7 @@ namespace lms::metadata
|
||||
}
|
||||
catch (av::Exception& e)
|
||||
{
|
||||
throw ParseException{};
|
||||
throw AudioFileParsingException{ e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +58,4 @@ namespace lms::metadata
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::avformat
|
||||
+2
-2
@@ -26,7 +26,7 @@ namespace lms::av
|
||||
class IAudioFile;
|
||||
}
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::avformat
|
||||
{
|
||||
class AvFormatImageReader : public IImageReader
|
||||
{
|
||||
@@ -42,4 +42,4 @@ namespace lms::metadata
|
||||
|
||||
std::unique_ptr<av::IAudioFile> _audioFile;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::avformat
|
||||
+6
-4
@@ -19,13 +19,13 @@
|
||||
|
||||
#include "AvFormatTagReader.hpp"
|
||||
|
||||
#include "av/Exception.hpp"
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "av/Types.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::avformat
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -165,10 +165,12 @@ namespace lms::metadata
|
||||
}
|
||||
catch (av::Exception& e)
|
||||
{
|
||||
throw ParseException{};
|
||||
throw AudioFileParsingException{ e.what() };
|
||||
}
|
||||
}
|
||||
|
||||
AvFormatTagReader::~AvFormatTagReader() = default;
|
||||
|
||||
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||
@@ -223,4 +225,4 @@ namespace lms::metadata
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::avformat
|
||||
+3
-3
@@ -25,13 +25,13 @@
|
||||
|
||||
#include "ITagReader.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::avformat
|
||||
{
|
||||
class AvFormatTagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
AvFormatTagReader(const std::filesystem::path& path, bool debug);
|
||||
~AvFormatTagReader() override = default;
|
||||
~AvFormatTagReader() override;
|
||||
AvFormatTagReader(const AvFormatTagReader&) = delete;
|
||||
AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
|
||||
|
||||
@@ -46,4 +46,4 @@ namespace lms::metadata
|
||||
av::IAudioFile::MetadataMap _metaDataMap;
|
||||
av::ContainerInfo _containerInfo;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::avformat
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 "Utils.hpp"
|
||||
|
||||
namespace lms::metadata::avformat::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
// TODO: use av capability to retrieve supported formats
|
||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||
".aac",
|
||||
".alac",
|
||||
".aif",
|
||||
".aiff",
|
||||
".ape",
|
||||
".dsf",
|
||||
".flac",
|
||||
".m4a",
|
||||
".m4b",
|
||||
".mp3",
|
||||
".mpc",
|
||||
".oga",
|
||||
".ogg",
|
||||
".opus",
|
||||
".shn",
|
||||
".wav",
|
||||
".wma",
|
||||
".wv",
|
||||
};
|
||||
return fileExtensions;
|
||||
}
|
||||
} // namespace lms::metadata::avformat::utils
|
||||
+6
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,14 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
#include "core/IResourceHandler.hpp"
|
||||
|
||||
namespace lms::av::transcoding
|
||||
namespace lms::metadata::avformat::utils
|
||||
{
|
||||
struct InputParameters;
|
||||
struct OutputParameters;
|
||||
|
||||
std::unique_ptr<IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
|
||||
} // namespace lms::av::transcoding
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
} // namespace lms::metadata::avformat::utils
|
||||
@@ -21,6 +21,10 @@
|
||||
|
||||
#include <taglib/taglib.h>
|
||||
|
||||
#if (TAGLIB_MAJOR_VERSION > 2)
|
||||
#define TAGLIB_HAS_DSF 1
|
||||
#endif
|
||||
|
||||
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
|
||||
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
|
||||
+18
-31
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "TagLibImageReader.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
@@ -34,24 +36,16 @@
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#include <tbytevector.h>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class ImageParsingFailedException : public Exception
|
||||
{
|
||||
};
|
||||
|
||||
Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
||||
{
|
||||
switch (type)
|
||||
@@ -349,44 +343,37 @@ namespace lms::metadata
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TagLib::FileRef parseFile(const std::filesystem::path& p)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForImages");
|
||||
|
||||
return TagLib::FileRef{ p.c_str(), false };
|
||||
}
|
||||
|
||||
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
|
||||
: _file{ parseFile(p) }
|
||||
: _file{ utils::parseFile(p, TagLib::AudioProperties::ReadStyle::Fast, utils::ReadAudioProperties{ false }) }
|
||||
{
|
||||
if (_file.isNull())
|
||||
if (!_file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw ImageParsingFailedException{};
|
||||
throw AudioFileParsingException{};
|
||||
}
|
||||
}
|
||||
|
||||
void TagLibImageReader::visitImages(ImageVisitor visitor) const
|
||||
{
|
||||
// MP3
|
||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
|
||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
|
||||
}
|
||||
// MP4
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||
{
|
||||
visitMP4Images(*mp4File, std::move(visitor));
|
||||
}
|
||||
// WMA
|
||||
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
|
||||
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||
{
|
||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
visitASFImages(*tag, std::move(visitor));
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // usage discouraged
|
||||
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
|
||||
@@ -394,38 +381,38 @@ namespace lms::metadata
|
||||
visitFLACImages(flacFile->pictureList(), std::move(visitor));
|
||||
}
|
||||
// Ogg vorbis
|
||||
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
|
||||
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.get()) })
|
||||
{
|
||||
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
|
||||
}
|
||||
// Ogg Opus
|
||||
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
|
||||
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.get()) })
|
||||
{
|
||||
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
|
||||
}
|
||||
// Aiff
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
|
||||
}
|
||||
// Wav
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||
{
|
||||
if (mpcFile->hasAPETag())
|
||||
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||
{
|
||||
if (wavPackFile->hasAPETag())
|
||||
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
|
||||
}
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::taglib
|
||||
+4
-4
@@ -21,9 +21,9 @@
|
||||
|
||||
#include "IImageReader.hpp"
|
||||
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::taglib
|
||||
{
|
||||
class TagLibImageReader : public IImageReader
|
||||
{
|
||||
@@ -32,6 +32,6 @@ namespace lms::metadata
|
||||
|
||||
void visitImages(ImageVisitor visitor) const override;
|
||||
|
||||
const TagLib::FileRef _file;
|
||||
std::unique_ptr<TagLib::File> _file;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::taglib
|
||||
+33
-50
@@ -19,44 +19,49 @@
|
||||
|
||||
#include "TagLibTagReader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/apeproperties.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#if TAGLIB_MAJOR_VERSION >= 2
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/oggflacfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/speexfile.h>
|
||||
#include <taglib/synchronizedlyricsframe.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/unsynchronizedlyricsframe.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class TagParsingFailedException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
// Mapping to internal taglib names and/or common alternative custom names
|
||||
@@ -166,21 +171,6 @@ namespace lms::metadata
|
||||
{ TagType::Writer, { "WRITER" } },
|
||||
};
|
||||
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast:
|
||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserReadStyle::Average:
|
||||
return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserReadStyle::Accurate:
|
||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
throw core::LmsException{ "Cannot convert read style" };
|
||||
}
|
||||
|
||||
void mergeTagMaps(TagLib::PropertyMap& dst, TagLib::PropertyMap&& src)
|
||||
{
|
||||
for (auto&& [tag, values] : src)
|
||||
@@ -211,35 +201,26 @@ namespace lms::metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForMetaData");
|
||||
|
||||
return TagLib::FileRef{ p.c_str(), true // read audio properties
|
||||
,
|
||||
readStyleToTagLibReadStyle(parserReadStyle) };
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TagLibTagReader::TagLibTagReader(const std::filesystem::path& p, ParserReadStyle parserReadStyle, bool debug)
|
||||
: _file{ parseFile(p, parserReadStyle) }
|
||||
: _file{ utils::parseFile(p, utils::readStyleToTagLibReadStyle(parserReadStyle), utils::ReadAudioProperties{ true }) }
|
||||
{
|
||||
if (_file.isNull())
|
||||
if (!_file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw TagParsingFailedException{};
|
||||
throw AudioFileParsingException{ "Parsing failed" };
|
||||
}
|
||||
|
||||
if (!_file.audioProperties())
|
||||
if (!_file->audioProperties())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||
throw TagParsingFailedException{};
|
||||
throw AudioFileNoAudioPropertiesException{};
|
||||
}
|
||||
|
||||
computeAudioProperties();
|
||||
|
||||
_propertyMap = _file.file()->properties();
|
||||
_propertyMap = _file->properties();
|
||||
|
||||
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
@@ -319,7 +300,7 @@ namespace lms::metadata
|
||||
};
|
||||
|
||||
// WMA
|
||||
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
|
||||
if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.get()) })
|
||||
{
|
||||
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
{
|
||||
@@ -365,7 +346,7 @@ namespace lms::metadata
|
||||
}
|
||||
}
|
||||
// MP3
|
||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
|
||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.get()) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
processID3v2Tags(*mp3File->ID3v2Tag());
|
||||
@@ -373,7 +354,7 @@ namespace lms::metadata
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
// MP4
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
|
||||
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.get()) })
|
||||
{
|
||||
// Taglib does not expose rtng in properties
|
||||
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||
@@ -406,40 +387,42 @@ namespace lms::metadata
|
||||
}
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.get()) })
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.get()) })
|
||||
{
|
||||
getAPETags(wavPackFile->APETag());
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.get()) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // discouraged usage
|
||||
processID3v2Tags(*flacFile->ID3v2Tag());
|
||||
}
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
|
||||
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.get()) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
processID3v2Tags(*aiffFile->tag());
|
||||
}
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
|
||||
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.get()) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
processID3v2Tags(*wavFile->ID3v2Tag());
|
||||
}
|
||||
}
|
||||
|
||||
TagLibTagReader::~TagLibTagReader() = default;
|
||||
|
||||
void TagLibTagReader::computeAudioProperties()
|
||||
{
|
||||
const TagLib::AudioProperties* properties{ _file.audioProperties() };
|
||||
const TagLib::AudioProperties* properties{ _file->audioProperties() };
|
||||
|
||||
// Common properties
|
||||
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||
_audioProperties.channelCount = static_cast<std::size_t>(_file.audioProperties()->channels());
|
||||
_audioProperties.channelCount = static_cast<std::size_t>(_file->audioProperties()->channels());
|
||||
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
|
||||
|
||||
@@ -457,7 +440,7 @@ namespace lms::metadata
|
||||
_audioProperties.bitsPerSample = aiffProperties->bitsPerSample();
|
||||
else if (const auto* wavProperties{ dynamic_cast<const TagLib::RIFF::WAV::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = wavProperties->bitsPerSample();
|
||||
#if TAGLIB_MAJOR_VERSION >= 2
|
||||
#if TAGLIB_HAS_DSF
|
||||
else if (const auto* dsfProperties{ dynamic_cast<const TagLib::DSF::Properties*>(properties) })
|
||||
_audioProperties.bitsPerSample = dsfProperties->bitsPerSample();
|
||||
#endif
|
||||
@@ -534,4 +517,4 @@ namespace lms::metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::taglib
|
||||
+6
-5
@@ -21,20 +21,21 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
|
||||
#include "ITagReader.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
namespace lms::metadata::taglib
|
||||
{
|
||||
class TagLibTagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
|
||||
~TagLibTagReader() = default;
|
||||
~TagLibTagReader() override;
|
||||
TagLibTagReader(const TagLibTagReader&) = delete;
|
||||
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
||||
|
||||
@@ -47,9 +48,9 @@ namespace lms::metadata
|
||||
|
||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||
|
||||
const TagLib::FileRef _file;
|
||||
std::unique_ptr<TagLib::File> _file;
|
||||
AudioProperties _audioProperties;
|
||||
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
} // namespace lms::metadata::taglib
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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 "Utils.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/audioproperties.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/id3v2framefactory.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/oggflacfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/speexfile.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tfilestream.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
namespace lms::metadata::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
static const std::vector<std::filesystem::path> supportedExtensions
|
||||
{
|
||||
".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus",
|
||||
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
|
||||
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
|
||||
".wav",
|
||||
#if TAGLIB_HAS_DSF
|
||||
".dsf", ".dff", ".dsdiff"
|
||||
#endif
|
||||
};
|
||||
|
||||
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||
}
|
||||
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserReadStyle::Fast:
|
||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserReadStyle::Average:
|
||||
return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserReadStyle::Accurate:
|
||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
throw Exception{ "Cannot convert read style" };
|
||||
}
|
||||
|
||||
TagLib::FileStream createFileStream(const std::filesystem::path& p)
|
||||
{
|
||||
FILE* file{ std::fopen(p.c_str(), "r") };
|
||||
if (!file)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fopen failed", ec };
|
||||
}
|
||||
|
||||
int fd{ ::fileno(file) };
|
||||
if (fd == -1)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fileno failed", ec };
|
||||
}
|
||||
|
||||
return TagLib::FileStream{ fd, true };
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||
{
|
||||
const std::string ext{ core::stringUtils::stringToUpper(extension.string().substr(1)) };
|
||||
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
// MP3
|
||||
if (ext == "MP3" || ext == "MP2" || ext == "AAC")
|
||||
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
// VORBIS
|
||||
else if (ext == "OGG")
|
||||
return std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "OGA")
|
||||
{
|
||||
/* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */
|
||||
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if (!file->isValid())
|
||||
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
else if (ext == "FLAC")
|
||||
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "SPX")
|
||||
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "OPUS")
|
||||
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// APE
|
||||
else if (ext == "MPC")
|
||||
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "WV")
|
||||
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "APE")
|
||||
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// TRUEAUDIO
|
||||
else if (ext == "TTA")
|
||||
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// MP4
|
||||
else if (ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V")
|
||||
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// ASF
|
||||
else if (ext == "WMA" || ext == "ASF")
|
||||
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// RIFF
|
||||
else if (ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC")
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "WAV")
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if TAGLIB_HAS_DSF
|
||||
else if (ext == "DSF")
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "DFF" || ext == "DSDIFF")
|
||||
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "File " << file << ": failed to parse by extension");
|
||||
file.reset();
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle, bool readAudioProperties)
|
||||
{
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
if (TagLib::MPEG::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
// VORBIS
|
||||
else if (TagLib::Ogg::Vorbis::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::FLAC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::FLAC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::Speex::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::Opus::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// APE
|
||||
else if (TagLib::MPC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::WavPack::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::APE::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// TRUEAUDIO
|
||||
else if (TagLib::TrueAudio::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// MP4
|
||||
else if (TagLib::MP4::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
//_ASF
|
||||
else if (TagLib::ASF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// RIFF
|
||||
else if (TagLib::RIFF::AIFF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::RIFF::WAV::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if TAGLIB_HAS_DSF
|
||||
else if (DSF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (DSDIFF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::DSDIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "File " << file << ": failed to parse by content");
|
||||
file.reset();
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||
|
||||
TagLib::FileStream fileStream{ createFileStream(p) };
|
||||
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), readStyle, readAudioProperties.value()) };
|
||||
if (!file)
|
||||
file = parseFileByContent(&fileStream, readStyle, readAudioProperties.value());
|
||||
|
||||
return file;
|
||||
}
|
||||
} // namespace lms::metadata::taglib::utils
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <taglib/audioproperties.h>
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "core/TaggedType.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle);
|
||||
|
||||
using ReadAudioProperties = core::TaggedBool<struct ReadAudioPropertiesTag>;
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, TagLib::AudioProperties::ReadStyle readStyle, ReadAudioProperties readAudioProperties);
|
||||
} // namespace lms::metadata::taglib::utils
|
||||
@@ -51,6 +51,6 @@ namespace lms::metadata
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedInfoFileExtensions();
|
||||
std::span<const std::filesystem::path> getSupportedArtistInfoFiles();
|
||||
ArtistInfo parseArtistInfo(std::istream& is);
|
||||
} // namespace lms::metadata
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <system_error>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
@@ -29,9 +31,31 @@ namespace lms::metadata
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
class ParseException : public Exception
|
||||
class IOException : public Exception
|
||||
{
|
||||
public:
|
||||
IOException(std::string_view message, std::error_code err)
|
||||
: Exception{ std::string{ message } + ": " + err.message() }
|
||||
, _err{ err }
|
||||
{
|
||||
}
|
||||
|
||||
std::error_code getErrorCode() const { return _err; }
|
||||
|
||||
private:
|
||||
std::error_code _err;
|
||||
};
|
||||
|
||||
class AudioFileParsingException : public Exception
|
||||
{
|
||||
public:
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class AudioFileNoAudioPropertiesException : public AudioFileParsingException
|
||||
{
|
||||
public:
|
||||
using AudioFileParsingException::AudioFileParsingException;
|
||||
};
|
||||
|
||||
} // namespace lms::metadata
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace lms::metadata::tests
|
||||
#PLAYLIST:My super playlist
|
||||
01-Foo.mp3
|
||||
|
||||
|
||||
|
||||
#EXTINF:263,Alice in Chains - Don't Follow
|
||||
02-FooBar.mp3
|
||||
|
||||
@@ -4,3 +4,4 @@ add_subdirectory(feedback)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(scanner)
|
||||
add_subdirectory(scrobbling)
|
||||
add_subdirectory(transcoding)
|
||||
|
||||
@@ -19,31 +19,27 @@
|
||||
|
||||
#include "ArtworkService.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Utils.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Artwork.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Image.hpp"
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
namespace lms::cover
|
||||
namespace lms::artwork
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
|
||||
{
|
||||
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath)
|
||||
{
|
||||
return std::make_unique<ArtworkService>(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath);
|
||||
@@ -92,59 +88,28 @@ namespace lms::cover
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseCover()
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseArtwork()
|
||||
{
|
||||
return _defaultReleaseCover;
|
||||
}
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistImage()
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistArtwork()
|
||||
{
|
||||
return _defaultArtistImage;
|
||||
}
|
||||
|
||||
bool ArtworkService::checkImageFile(const std::filesystem::path& filePath)
|
||||
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::size_t index, std::optional<image::ImageSize> width) const
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
if (!isFileSupported(filePath, _fileExtensions))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(filePath, ec))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::is_regular_file(filePath, ec))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
|
||||
{
|
||||
struct CandidateImage
|
||||
{
|
||||
std::unique_ptr<image::IEncodedImage> image;
|
||||
bool isFront{};
|
||||
std::size_t index;
|
||||
|
||||
// > means is better candidate
|
||||
bool operator>(const CandidateImage& other) const
|
||||
{
|
||||
if (!isFront && other.isFront)
|
||||
return false;
|
||||
if (isFront && !other.isFront)
|
||||
return true;
|
||||
|
||||
return index < other.index;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<CandidateImage> candidateImages;
|
||||
std::size_t pictureIndex{};
|
||||
std::unique_ptr<image::IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
std::size_t currentIndex{};
|
||||
|
||||
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
|
||||
std::unique_ptr<image::IEncodedImage> image;
|
||||
if (currentIndex++ != index)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!width)
|
||||
@@ -162,8 +127,6 @@ namespace lms::cover
|
||||
{
|
||||
LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what());
|
||||
}
|
||||
|
||||
candidateImages.emplace_back(CandidateImage{ .image = std::move(image), .isFront = parsedImage.type == metadata::Image::Type::FrontCover, .index = pictureIndex++ });
|
||||
});
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
@@ -171,63 +134,108 @@ namespace lms::cover
|
||||
LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
|
||||
}
|
||||
|
||||
std::stable_sort(std::begin(candidateImages), std::end(candidateImages), std::greater<>());
|
||||
if (!candidateImages.empty())
|
||||
return std::move(candidateImages.front().image);
|
||||
return image;
|
||||
}
|
||||
|
||||
return {};
|
||||
db::ArtworkId ArtworkService::findTrackListImage(db::TrackListId trackListId)
|
||||
{
|
||||
db::ArtworkId artworkId;
|
||||
|
||||
// Iterate over all tracks and stop when we find an artwork
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::TrackList::pointer trackList{ db::TrackList::find(session, trackListId) };
|
||||
if (!trackList)
|
||||
return artworkId;
|
||||
|
||||
const auto entries{ trackList->getEntries(db::Range{ 0, 10 }) };
|
||||
for (const auto& entry : entries.results)
|
||||
{
|
||||
const auto track{ entry->getTrack() };
|
||||
if (track->getPreferredMediaArtworkId().isValid())
|
||||
{
|
||||
artworkId = track->getPreferredMediaArtworkId();
|
||||
break; // stop iteration
|
||||
}
|
||||
|
||||
if (track->getPreferredArtworkId().isValid())
|
||||
{
|
||||
artworkId = track->getPreferredArtworkId();
|
||||
break; // stop iteration
|
||||
}
|
||||
}
|
||||
|
||||
return artworkId;
|
||||
}
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width)
|
||||
{
|
||||
const ImageCache::EntryDesc cacheEntryDesc{ artworkId, width };
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
|
||||
if (image)
|
||||
return image;
|
||||
|
||||
db::TrackEmbeddedImageId trackEmbeddedImageId;
|
||||
db::ImageId imageId;
|
||||
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) };
|
||||
if (artwork)
|
||||
{
|
||||
trackEmbeddedImageId = artwork->getTrackEmbeddedImageId();
|
||||
imageId = artwork->getImageId();
|
||||
}
|
||||
}
|
||||
|
||||
if (trackEmbeddedImageId.isValid())
|
||||
image = getTrackEmbeddedImage(trackEmbeddedImageId, width);
|
||||
else if (imageId.isValid())
|
||||
image = getImage(imageId, width);
|
||||
|
||||
if (image)
|
||||
_cache.addImage(cacheEntryDesc, image);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
|
||||
{
|
||||
const ImageCache::EntryDesc cacheEntryDesc{ imageId, width };
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
std::filesystem::path imageFile;
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const db::Image::pointer image{ db::Image::find(session, imageId) };
|
||||
if (image)
|
||||
imageFile = image->getAbsoluteFilePath();
|
||||
if (!image)
|
||||
return nullptr;
|
||||
|
||||
imageFile = image->getAbsoluteFilePath();
|
||||
}
|
||||
|
||||
cover = getFromImageFile(imageFile, width);
|
||||
if (cover)
|
||||
_cache.addImage(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
return getFromImageFile(imageFile, width);
|
||||
}
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width)
|
||||
{
|
||||
const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width };
|
||||
std::shared_ptr<image::IEncodedImage> image;
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
||||
if (cover)
|
||||
return cover;
|
||||
|
||||
std::filesystem::path trackFile;
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::Track::FindParameters params;
|
||||
params.setEmbeddedImage(trackEmbeddedImageId);
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
if (!cover)
|
||||
cover = getTrackImage(track->getAbsoluteFilePath(), width);
|
||||
// TODO: could be put outside transaction
|
||||
db::TrackEmbeddedImageLink::find(session, trackEmbeddedImageId, [&](const db::TrackEmbeddedImageLink::pointer& link) {
|
||||
if (!image)
|
||||
image = getTrackImage(link->getTrack()->getAbsoluteFilePath(), link->getIndex(), width);
|
||||
});
|
||||
}
|
||||
|
||||
if (cover)
|
||||
_cache.addImage(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
return image;
|
||||
}
|
||||
|
||||
void ArtworkService::flushCache()
|
||||
@@ -241,5 +249,4 @@ namespace lms::cover
|
||||
|
||||
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
||||
}
|
||||
|
||||
} // namespace lms::cover
|
||||
} // namespace lms::artwork
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
#include "services/artwork/IArtworkService.hpp"
|
||||
|
||||
#include "ImageCache.hpp"
|
||||
@@ -36,7 +38,7 @@ namespace lms::metadata
|
||||
class IAudioFileParser;
|
||||
}
|
||||
|
||||
namespace lms::cover
|
||||
namespace lms::artwork
|
||||
{
|
||||
class ArtworkService : public IArtworkService
|
||||
{
|
||||
@@ -47,19 +49,21 @@ namespace lms::cover
|
||||
ArtworkService& operator=(const ArtworkService&) = delete;
|
||||
|
||||
private:
|
||||
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
|
||||
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) override;
|
||||
db::ArtworkId findTrackListImage(db::TrackListId trackListId) override;
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
|
||||
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
|
||||
std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) override;
|
||||
|
||||
std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() override;
|
||||
std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() override;
|
||||
|
||||
void flushCache() override;
|
||||
void setJpegQuality(unsigned quality) override;
|
||||
|
||||
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
|
||||
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::optional<image::ImageSize> width) const;
|
||||
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width);
|
||||
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width);
|
||||
|
||||
static bool checkImageFile(const std::filesystem::path& filePath);
|
||||
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
|
||||
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::size_t index, std::optional<image::ImageSize> width) const;
|
||||
|
||||
db::Db& _db;
|
||||
|
||||
@@ -72,4 +76,4 @@ namespace lms::cover
|
||||
unsigned _jpegQuality;
|
||||
};
|
||||
|
||||
} // namespace lms::cover
|
||||
} // namespace lms::artwork
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Random.hpp"
|
||||
|
||||
namespace lms::cover
|
||||
namespace lms::artwork
|
||||
{
|
||||
ImageCache::ImageCache(std::size_t maxCacheSize)
|
||||
: _maxCacheSize{ maxCacheSize }
|
||||
@@ -79,4 +79,4 @@ namespace lms::cover
|
||||
_cacheSize = 0;
|
||||
_cache.clear();
|
||||
}
|
||||
} // namespace lms::cover
|
||||
} // namespace lms::artwork
|
||||
@@ -25,13 +25,11 @@
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace lms::cover
|
||||
namespace lms::artwork
|
||||
{
|
||||
class ImageCache
|
||||
{
|
||||
@@ -40,8 +38,7 @@ namespace lms::cover
|
||||
|
||||
struct EntryDesc
|
||||
{
|
||||
using VariantType = std::variant<db::TrackEmbeddedImageId, db::ImageId>;
|
||||
VariantType id;
|
||||
db::ArtworkId id;
|
||||
std::optional<std::size_t> size;
|
||||
|
||||
bool operator==(const EntryDesc& other) const = default;
|
||||
@@ -63,13 +60,13 @@ namespace lms::cover
|
||||
std::size_t operator()(const EntryDesc& entry) const
|
||||
{
|
||||
assert(entry.size); // should not cache unresized images
|
||||
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
|
||||
return std::hash<db::ArtworkId>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
|
||||
std::size_t _cacheSize{};
|
||||
mutable std::atomic<std::size_t> _cacheMisses{};
|
||||
mutable std::atomic<std::size_t> _cacheHits{};
|
||||
mutable std::atomic<std::size_t> _cacheMisses;
|
||||
mutable std::atomic<std::size_t> _cacheHits;
|
||||
};
|
||||
} // namespace lms::cover
|
||||
} // namespace lms::artwork
|
||||
@@ -23,8 +23,8 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
#include "database/ArtworkId.hpp"
|
||||
#include "database/TrackListId.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace lms::db
|
||||
@@ -32,19 +32,22 @@ namespace lms::db
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace lms::cover
|
||||
namespace lms::artwork
|
||||
{
|
||||
class IArtworkService
|
||||
{
|
||||
public:
|
||||
virtual ~IArtworkService() = default;
|
||||
|
||||
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;
|
||||
virtual std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) = 0;
|
||||
// Helpers to get preferred artworks
|
||||
virtual db::ArtworkId findTrackListImage(db::TrackListId trackListId) = 0;
|
||||
|
||||
// Svg images dont have image "size"
|
||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
|
||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() = 0;
|
||||
// Image retrieval, no width means original size
|
||||
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) = 0;
|
||||
|
||||
// Svg images don't have image "size"
|
||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() = 0;
|
||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() = 0;
|
||||
|
||||
virtual void flushCache() = 0;
|
||||
|
||||
@@ -53,4 +56,4 @@ namespace lms::cover
|
||||
|
||||
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||
|
||||
} // namespace lms::cover
|
||||
} // namespace lms::artwork
|
||||
|
||||
@@ -18,14 +18,11 @@ target_include_directories(lmsauth PRIVATE
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PRIVATE
|
||||
lmscore
|
||||
lmsdatabase
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PUBLIC
|
||||
Boost::system
|
||||
Wt::Wt
|
||||
lmscore
|
||||
lmsdatabase
|
||||
)
|
||||
|
||||
# PAM
|
||||
|
||||
@@ -21,7 +21,11 @@ target_include_directories(lmsrecommendation PRIVATE
|
||||
)
|
||||
|
||||
target_link_libraries(lmsrecommendation PRIVATE
|
||||
lmsdatabase
|
||||
|
||||
lmssom
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
target_link_libraries(lmsrecommendation PUBLIC
|
||||
lmsdatabase
|
||||
)
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "PlaylistGeneratorService.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
+2
@@ -23,6 +23,8 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/EnumSet.hpp"
|
||||
#include "database/ArtistId.hpp"
|
||||
#include "database/ReleaseId.hpp"
|
||||
#include "database/TrackListId.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "services/recommendation/Types.hpp"
|
||||
|
||||
@@ -2,17 +2,21 @@ add_library(lmsscanner STATIC
|
||||
impl/helpers/ArtistHelpers.cpp
|
||||
impl/scanners/ArtistInfoFileScanner.cpp
|
||||
impl/scanners/AudioFileScanOperation.cpp
|
||||
impl/scanners/FileScanOperationBase.cpp
|
||||
impl/scanners/AudioFileScanner.cpp
|
||||
impl/scanners/ImageFileScanner.cpp
|
||||
impl/scanners/LyricsFileScanner.cpp
|
||||
impl/scanners/PlayListFileScanner.cpp
|
||||
impl/scanners/Utils.cpp
|
||||
impl/steps/ArtworkUtils.cpp
|
||||
impl/steps/FileScanQueue.cpp
|
||||
impl/steps/ScanErrorLogger.cpp
|
||||
impl/steps/ScanStepArtistReconciliation.cpp
|
||||
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||
impl/steps/ScanStepAssociateTrackImages.cpp
|
||||
impl/steps/ScanStepBase.cpp
|
||||
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user