Merge branch 'develop' for release v3.59.0
This commit is contained in:
+6
-4
@@ -129,7 +129,10 @@ deploy-path = "/newroot/"; # ending slash is important
|
|||||||
```
|
```
|
||||||
If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result.
|
If you use nginx as a reverse proxy, you can simply replace `location /` with `location /newroot/` to achieve the same result.
|
||||||
## Reverse proxy settings
|
## Reverse proxy settings
|
||||||
_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the _behind-reverse-proxy_ option to _true_ in the `lms.conf` configuration file.
|
_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the `behind-reverse-proxy` option to _true_ in the `lms.conf` configuration file and to adjust the trusted proxy list in `trusted-proxies` option.
|
||||||
|
|
||||||
|
__Note__: when running in a docker environment, you have to trust the docker gateway IP (which is `172.17.0.1` by default)
|
||||||
|
|
||||||
Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_:
|
Here is an example to make _LMS_ properly work on _myserver.org_ using _nginx_:
|
||||||
```
|
```
|
||||||
server {
|
server {
|
||||||
@@ -147,10 +150,9 @@ server {
|
|||||||
keepalive_timeout 10m;
|
keepalive_timeout 10m;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
||||||
proxy_set_header Client-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
proxy_pass http://localhost:5082/;
|
proxy_pass http://localhost:5082/;
|
||||||
|
|||||||
@@ -43,15 +43,15 @@ _LMS_ primarily relies on tags to organize your music collection but also suppor
|
|||||||
### Filtering
|
### 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.
|
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.
|
||||||
|
|
||||||
__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
|
__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_.
|
||||||
|
|
||||||
### Multiple artists
|
### Multiple artists
|
||||||
_LMS_ works best when using the default Picard settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
|
_LMS_ works best when using the default [Picard](https://picard.musicbrainz.org/) settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
|
||||||
|
|
||||||
### Multiple album artists
|
### Multiple album artists
|
||||||
While LMS can manage multiple album artists using the `albumartist` tag, it works better when using the custom `albumartists` and `albumartistssort` tags, similar to how it handles regular artist tags.
|
While LMS can manage multiple album artists using the `albumartist` tag, it works better when using the custom `albumartists` and `albumartistssort` tags, similar to how it handles regular artist tags.
|
||||||
|
|
||||||
__Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
|
__Note__: if you use Picard, add the following script to include these tags:
|
||||||
```
|
```
|
||||||
$setmulti(albumartists,%_albumartists%)
|
$setmulti(albumartists,%_albumartists%)
|
||||||
$setmulti(albumartistssort,%_albumartists_sort%)
|
$setmulti(albumartistssort,%_albumartists_sort%)
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@
|
|||||||
${</if-has-non-release-tracks>}
|
${</if-has-non-release-tracks>}
|
||||||
${<if-has-similar-artists>}
|
${<if-has-similar-artists>}
|
||||||
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
<h3>${tr:Lms.Explore.Artist.similar-artists}</h3>
|
||||||
${similar-artists class="d-grid gap-3"}
|
${similar-artists class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
||||||
${</if-has-similar-artists>}
|
${</if-has-similar-artists>}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -14,11 +14,18 @@
|
|||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Artists.template.entry">
|
<message id="Lms.Explore.Artists.template.entry">
|
||||||
${name class="text-decoration-none link-secondary"}
|
<div class="col h-100 Lms-bg-dark-hover rounded">
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="Lms-responsive-square Lms-cover-container mb-1">
|
||||||
|
${image class="shadow-sm"}
|
||||||
|
</div>
|
||||||
|
${name class="d-block text-center text-truncate text-decoration-none link-secondary"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Artists.template.container">
|
<message id="Lms.Explore.Artists.template.container">
|
||||||
${elements class="d-grid gap-3"}
|
${elements class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-6 gx-2 gy-4"}
|
||||||
${loading-indicator}
|
${loading-indicator}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="256px" height="256px" viewBox="0 0 16.00 16.00" xmlns="http://www.w3.org/2000/svg" fill="#c9c9c9" stroke="#c9c9c9" stroke-width="0.00016" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.6719999999999999"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<g color="#c9c9c9" font-weight="400" font-family="Ubuntu" letter-spacing="0" word-spacing="0" white-space="normal" fill="#c9c9c9">
|
||||||
|
<path d="M8 2a2.84 2.84 0 0 0-1.12.221c-.345.141-.651.348-.906.615v.003l-.001.002c-.248.269-.44.592-.574.96-.137.367-.203.769-.203 1.2 0 .435.065.841.203 1.209.135.361.327.68.574.95l.001.002c.254.267.558.477.901.624v.003c.346.141.723.21 1.12.21.395 0 .77-.069 1.117-.21v-.002c.343-.147.644-.357.892-.625.255-.268.45-.59.586-.952.138-.368.204-.774.204-1.21h.01c0-.43-.065-.831-.203-1.198a2.771 2.771 0 0 0-.585-.963 2.5 2.5 0 0 0-.897-.618A2.835 2.835 0 0 0 7.999 2zM8.024 10.002c-2.317 0-3.561.213-4.486.91-.462.35-.767.825-.939 1.378-.172.553-.226.975-.228 1.71L8 14.002h5.629c-.001-.736-.052-1.159-.225-1.712-.172-.553-.477-1.027-.94-1.376-.923-.697-2.124-.912-4.44-.912z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -110,6 +110,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled on {1}</message>
|
<message id="Lms.Admin.ScannerController.status-scheduled">Scheduled on {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associating artist images: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release 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-duplicate-files">Checking for duplicate files... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
||||||
@@ -276,18 +277,24 @@
|
|||||||
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
<message id="Lms.PlayHistory.playhistory">Play History</message>
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
|
<message id="Lms.Settings.artist-release-sort-method">Method to sort artist's albums</message>
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
||||||
<message id="Lms.Settings.backend.internal">Internal</message>
|
<message id="Lms.Settings.backend.internal">Internal</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
|
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
|
||||||
<message id="Lms.Settings.change-password">Change password</message>
|
<message id="Lms.Settings.change-password">Change password</message>
|
||||||
|
<message id="Lms.Settings.date-asc">Release Date (Ascending)</message>
|
||||||
|
<message id="Lms.Settings.date-desc">Release Date (Descending)</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-bitrate">Default transcoding output bitrate</message>
|
<message id="Lms.Settings.default-transcoding-output-bitrate">Default transcoding output bitrate</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-format">Default transcoding output format</message>
|
<message id="Lms.Settings.default-transcoding-output-format">Default transcoding output format</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||||
<message id="Lms.Settings.enable-transcoding-by-default">Enable transcoding by default</message>
|
<message id="Lms.Settings.enable-transcoding-by-default">Enable transcoding by default</message>
|
||||||
<message id="Lms.Settings.feedback">Feedback</message>
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||||
|
<message id="Lms.Settings.name">Album Name</message>
|
||||||
|
<message id="Lms.Settings.original-date-asc">Original Release Date (Ascending)</message>
|
||||||
|
<message id="Lms.Settings.original-date-desc">Original Release Date (Descending)</message>
|
||||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
<message id="Lms.Settings.replaygain-mode">ReplayGain mode</message>
|
||||||
@@ -305,6 +312,7 @@
|
|||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message>
|
||||||
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
||||||
|
<message id="Lms.Settings.user-interface">User Interface</message>
|
||||||
<message id="Lms.Settings.transcoding">Transcoding</message>
|
<message id="Lms.Settings.transcoding">Transcoding</message>
|
||||||
<message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
|
<message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
|
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié le {1}</message>
|
<message id="Lms.Admin.ScannerController.status-scheduled">Planifié le {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {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-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
||||||
@@ -276,18 +277,24 @@
|
|||||||
<message id="Lms.PlayHistory.playhistory">Historique de lecture</message>
|
<message id="Lms.PlayHistory.playhistory">Historique de lecture</message>
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
|
<message id="Lms.Settings.artist-release-sort-method">Méthode pour trier les albums de l'artiste</message>
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
||||||
<message id="Lms.Settings.backend.internal">Interne</message>
|
<message id="Lms.Settings.backend.internal">Interne</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
|
||||||
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
||||||
|
<message id="Lms.Settings.date-asc">Date de sortie (Croissant)</message>
|
||||||
|
<message id="Lms.Settings.date-desc">Date de sortie (Décroissant)</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate par défaut du transcodage</message>
|
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate par défaut du transcodage</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-format">Format par défaut du transcodage</message>
|
<message id="Lms.Settings.default-transcoding-output-format">Format par défaut du transcodage</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||||
<message id="Lms.Settings.enable-transcoding-by-default">Activer par défaut le transcodage</message>
|
<message id="Lms.Settings.enable-transcoding-by-default">Activer par défaut le transcodage</message>
|
||||||
<message id="Lms.Settings.feedback">Feedback</message>
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
||||||
|
<message id="Lms.Settings.name">Nom de l'album</message>
|
||||||
|
<message id="Lms.Settings.original-date-asc">Date de sortie originale (Croissant)</message>
|
||||||
|
<message id="Lms.Settings.original-date-desc">Date de sortie originale (Décroissant)</message>
|
||||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
<message id="Lms.Settings.replaygain-mode">Mode ReplayGain</message>
|
||||||
@@ -305,6 +312,7 @@
|
|||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
|
||||||
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
|
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
|
||||||
|
<message id="Lms.Settings.user-interface">Interface utilisateur</message>
|
||||||
<message id="Lms.Settings.transcoding">Transcodage</message>
|
<message id="Lms.Settings.transcoding">Transcodage</message>
|
||||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
|
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
|
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-scheduled">Pianificato il {1}</message>
|
<message id="Lms.Admin.ScannerController.status-scheduled">Pianificato il {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Associando immagini degli artisti: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {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-duplicate-files">Controllo duplicati... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
||||||
@@ -276,18 +277,24 @@
|
|||||||
<message id="Lms.PlayHistory.playhistory">Storico riproduzione</message>
|
<message id="Lms.PlayHistory.playhistory">Storico riproduzione</message>
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
|
<message id="Lms.Settings.artist-release-sort-method">Metodo per ordinare gli album dell'artista</message>
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
||||||
<message id="Lms.Settings.backend.internal">Interno</message>
|
<message id="Lms.Settings.backend.internal">Interno</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz-token">Token API ListenBrainz</message>
|
||||||
<message id="Lms.Settings.change-password">Cambia password</message>
|
<message id="Lms.Settings.change-password">Cambia password</message>
|
||||||
|
<message id="Lms.Settings.date-asc">Data di uscita (Crescente)</message>
|
||||||
|
<message id="Lms.Settings.date-desc">Data di uscita (Decrescente)</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate di default per la transcodifica</message>
|
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate di default per la transcodifica</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-format">Formato di default per la transcodifica</message>
|
<message id="Lms.Settings.default-transcoding-output-format">Formato di default per la transcodifica</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
|
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
|
||||||
<message id="Lms.Settings.enable-transcoding-by-default">Abilita transcodifica di default</message>
|
<message id="Lms.Settings.enable-transcoding-by-default">Abilita transcodifica di default</message>
|
||||||
<message id="Lms.Settings.feedback">Feedback</message>
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
||||||
|
<message id="Lms.Settings.name">Nome dell'album</message>
|
||||||
|
<message id="Lms.Settings.original-date-asc">Data di uscita originale (Crescente)</message>
|
||||||
|
<message id="Lms.Settings.original-date-desc">Data di uscita originale (Decrescente)</message>
|
||||||
<message id="Lms.Settings.password-bad">Password errata</message>
|
<message id="Lms.Settings.password-bad">Password errata</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">Modalità ReplayGain</message>
|
<message id="Lms.Settings.replaygain-mode">Modalità ReplayGain</message>
|
||||||
@@ -305,6 +312,7 @@
|
|||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message>
|
||||||
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
||||||
|
<message id="Lms.Settings.user-interface">Interfaccia utente</message>
|
||||||
<message id="Lms.Settings.transcoding">Transcodifica</message>
|
<message id="Lms.Settings.transcoding">Transcodifica</message>
|
||||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
|
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
|
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano na {1}</message>
|
<message id="Lms.Admin.ScannerController.status-scheduled">Zaplanowano na {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">Skanowanie: krok {1}/{2}</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Kojarzenie obrazów artystów: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
||||||
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
||||||
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
|
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
|
||||||
@@ -302,18 +303,24 @@
|
|||||||
<message id="Lms.PlayHistory.playhistory">Historia odtwarzania</message>
|
<message id="Lms.PlayHistory.playhistory">Historia odtwarzania</message>
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
|
<message id="Lms.Settings.artist-release-sort-method">Metoda sortowania albumów artysty</message>
|
||||||
<message id="Lms.Settings.audio">Dźwięk</message>
|
<message id="Lms.Settings.audio">Dźwięk</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">Te ustawienia dotyczą wyłącznie tej przeglądarki!</message>
|
<message id="Lms.Settings.audio-settings-are-local">Te ustawienia dotyczą wyłącznie tej przeglądarki!</message>
|
||||||
<message id="Lms.Settings.backend.internal">Wewnętrzny</message>
|
<message id="Lms.Settings.backend.internal">Wewnętrzny</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
|
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
|
||||||
<message id="Lms.Settings.change-password">Zmień hasło</message>
|
<message id="Lms.Settings.change-password">Zmień hasło</message>
|
||||||
|
<message id="Lms.Settings.date-asc">Data wydania (Rosnąco)</message>
|
||||||
|
<message id="Lms.Settings.date-desc">Data wydania (Malejąco)</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-bitrate">Domyślna przepływność transkodowania</message>
|
<message id="Lms.Settings.default-transcoding-output-bitrate">Domyślna przepływność transkodowania</message>
|
||||||
<message id="Lms.Settings.default-transcoding-output-format">Domyślny format wyjściowy transkodowania</message>
|
<message id="Lms.Settings.default-transcoding-output-format">Domyślny format wyjściowy transkodowania</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Nie można zapisywać na koncie demonstracyjnym!</message>
|
<message id="Lms.Settings.demo-cannot-save">Nie można zapisywać na koncie demonstracyjnym!</message>
|
||||||
<message id="Lms.Settings.enable-transcoding-by-default">Aktywuj domyślnie transkodowanie</message>
|
<message id="Lms.Settings.enable-transcoding-by-default">Aktywuj domyślnie transkodowanie</message>
|
||||||
<message id="Lms.Settings.feedback">Feedback</message>
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ustawienia</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ustawienia</message>
|
||||||
|
<message id="Lms.Settings.name">Nazwa albumu</message>
|
||||||
|
<message id="Lms.Settings.original-date-asc">Oryginalna data wydania (Rosnąco)</message>
|
||||||
|
<message id="Lms.Settings.original-date-desc">Oryginalna data wydania (Malejąco)</message>
|
||||||
<message id="Lms.Settings.password-bad">Błędne hasło</message>
|
<message id="Lms.Settings.password-bad">Błędne hasło</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Należy podać stare hasło</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Należy podać stare hasło</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">Tryb ReplayGain</message>
|
<message id="Lms.Settings.replaygain-mode">Tryb ReplayGain</message>
|
||||||
@@ -331,6 +338,7 @@
|
|||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artyści z albumów</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artyści z albumów</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artyści ze ścieżek</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artyści ze ścieżek</message>
|
||||||
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
<message id="Lms.Settings.subsonic-api">Subsonic API</message>
|
||||||
|
<message id="Lms.Settings.user-interface">Interfejs użytkownika</message>
|
||||||
<message id="Lms.Settings.transcoding">Transkodowanie</message>
|
<message id="Lms.Settings.transcoding">Transkodowanie</message>
|
||||||
<message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message>
|
<message id="Lms.Settings.transcoding-output-bitrate">Przepływność transkodowania</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
|
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
|
||||||
|
|||||||
+10
-1
@@ -80,6 +80,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.Database.update-period">更新周期</message>
|
<message id="Lms.Admin.Database.update-period">更新周期</message>
|
||||||
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
|
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
|
||||||
<message id="Lms.Admin.Database.weekly">每周</message>
|
<message id="Lms.Admin.Database.weekly">每周</message>
|
||||||
@@ -110,9 +111,10 @@
|
|||||||
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {1}</message>
|
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {1}</message>
|
||||||
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
|
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
|
||||||
|
|
||||||
|
|
||||||
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
|
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
|
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
|
||||||
@@ -276,6 +278,7 @@
|
|||||||
<message id="Lms.PlayHistory.playhistory">播放历史</message>
|
<message id="Lms.PlayHistory.playhistory">播放历史</message>
|
||||||
|
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
|
|
||||||
<message id="Lms.Settings.audio">音频</message>
|
<message id="Lms.Settings.audio">音频</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||||
|
|
||||||
@@ -284,10 +287,15 @@
|
|||||||
<message id="Lms.Settings.change-password">更改密码</message>
|
<message id="Lms.Settings.change-password">更改密码</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.password-bad">无效密码</message>
|
<message id="Lms.Settings.password-bad">无效密码</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
|
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
|
||||||
<message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message>
|
<message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message>
|
||||||
@@ -305,6 +313,7 @@
|
|||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
|
||||||
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
|
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
|
||||||
|
|
||||||
<message id="Lms.Settings.transcoding">转码</message>
|
<message id="Lms.Settings.transcoding">转码</message>
|
||||||
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
|
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format">转码格式</message>
|
<message id="Lms.Settings.transcoding-output-format">转码格式</message>
|
||||||
|
|||||||
+14
-3
@@ -5,10 +5,21 @@
|
|||||||
|
|
||||||
<message id="Lms.Settings.template">
|
<message id="Lms.Settings.template">
|
||||||
<form class="row g-3">
|
<form class="row g-3">
|
||||||
<legend>${tr:Lms.Settings.audio}</legend>
|
<legend>${tr:Lms.Settings.user-interface}</legend>
|
||||||
<div class="alert alert-info">
|
<div class="col-12">
|
||||||
${tr:Lms.Settings.audio-settings-are-local}
|
<label class="form-label" for="${id:artist-release-sort-method}">
|
||||||
|
${tr:Lms.Settings.artist-release-sort-method}
|
||||||
|
</label>
|
||||||
|
${artist-release-sort-method class="form-control"}
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
${artist-release-sort-method-info}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<legend>${tr:Lms.Settings.audio}
|
||||||
|
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.audio-settings-are-local}">
|
||||||
|
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</legend>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<label class="form-label" for="${id:transcoding-mode}">
|
<label class="form-label" for="${id:transcoding-mode}">
|
||||||
${tr:Lms.Settings.transcoding-mode}
|
${tr:Lms.Settings.transcoding-mode}
|
||||||
|
|||||||
+8
-4
@@ -19,6 +19,13 @@ db-show-queries = false;
|
|||||||
listen-port = 5082;
|
listen-port = 5082;
|
||||||
listen-addr = "0.0.0.0";
|
listen-addr = "0.0.0.0";
|
||||||
behind-reverse-proxy = false;
|
behind-reverse-proxy = false;
|
||||||
|
# original-ip-header and trusted-proxies are used only if behind-reverse-proxy is set to true
|
||||||
|
original-ip-header = "X-Forwarded-For";
|
||||||
|
trusted-proxies =
|
||||||
|
(
|
||||||
|
"127.0.0.1",
|
||||||
|
"::1"
|
||||||
|
);
|
||||||
|
|
||||||
# If enabled, these files have to exist and have correct permissions
|
# If enabled, these files have to exist and have correct permissions
|
||||||
tls-enable = false;
|
tls-enable = false;
|
||||||
@@ -74,9 +81,6 @@ api-open-subsonic-disabled-clients = ("DSub");
|
|||||||
# Turn on this option to allow the demo account creation/use
|
# Turn on this option to allow the demo account creation/use
|
||||||
demo = false;
|
demo = false;
|
||||||
|
|
||||||
# Max external cover file size in MBytes
|
|
||||||
cover-max-file-size = 10;
|
|
||||||
|
|
||||||
# Max cover cache size in MBytes
|
# Max cover cache size in MBytes
|
||||||
cover-max-cache-size = 30;
|
cover-max-cache-size = 30;
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ cover-max-cache-size = 30;
|
|||||||
cover-jpeg-quality = 75;
|
cover-jpeg-quality = 75;
|
||||||
|
|
||||||
# Preferred file names for covers (order is important)
|
# Preferred file names for covers (order is important)
|
||||||
cover-preferred-file-names = ("cover", "front");
|
cover-preferred-file-names = ("cover", "front", "folder", "default");
|
||||||
|
|
||||||
# File names for artist images (order is important)
|
# File names for artist images (order is important)
|
||||||
# Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries.
|
# Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace lms::core
|
||||||
|
{
|
||||||
|
static inline const std::filesystem::path sysconfDirectory{ "/etc" };
|
||||||
|
}
|
||||||
@@ -277,7 +277,7 @@ namespace lms::db
|
|||||||
|
|
||||||
ObjectPtr<Image> Artist::getImage() const
|
ObjectPtr<Image> Artist::getImage() const
|
||||||
{
|
{
|
||||||
return ObjectPtr<Image>{ _image.lock() };
|
return ObjectPtr<Image>{ _image };
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||||
@@ -379,5 +379,4 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
_image = getDboPtr(image);
|
_image = getDboPtr(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
@@ -40,7 +41,7 @@ namespace lms::db
|
|||||||
if (params.directory.isValid())
|
if (params.directory.isValid())
|
||||||
query.where("i.directory_id = ?").bind(params.directory);
|
query.where("i.directory_id = ?").bind(params.directory);
|
||||||
if (!params.fileStem.empty())
|
if (!params.fileStem.empty())
|
||||||
query.where("i.stem = ?").bind(params.fileStem);
|
query.where("i.stem = ? COLLATE NOCASE").bind(params.fileStem);
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 67 };
|
static constexpr Version LMS_DATABASE_VERSION{ 70 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -761,9 +761,126 @@ SELECT
|
|||||||
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN cur_playing_track_pos");
|
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN cur_playing_track_pos");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV67(Session& session)
|
||||||
|
{
|
||||||
|
// Add a ref to release in image
|
||||||
|
session.getDboSession()->execute(R"(CREATE TABLE "image_backup" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"absolute_file_path" text not null,
|
||||||
|
"stem" text not null,
|
||||||
|
"file_last_write" text,
|
||||||
|
"file_size" integer not null,
|
||||||
|
"width" integer not null,
|
||||||
|
"height" integer not null,
|
||||||
|
"artist_id" bigint,
|
||||||
|
"release_id" bigint,
|
||||||
|
"directory_id" bigint,
|
||||||
|
constraint "fk_image_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_image_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_image_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
// Migrate data, with the new release_id field set to null
|
||||||
|
session.getDboSession()->execute(R"(INSERT INTO image_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
absolute_file_path,
|
||||||
|
stem,
|
||||||
|
file_last_write,
|
||||||
|
file_size,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
artist_id,
|
||||||
|
NULL,
|
||||||
|
directory_id
|
||||||
|
FROM image
|
||||||
|
)");
|
||||||
|
session.getDboSession()->execute("DROP TABLE image");
|
||||||
|
session.getDboSession()->execute("ALTER TABLE image_backup RENAME TO image");
|
||||||
|
|
||||||
|
// Changed some indexes for the image table -> remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
|
||||||
|
std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) };
|
||||||
|
for (const auto& indexName : indexeNames)
|
||||||
|
session.getDboSession()->execute("DROP INDEX " + indexName);
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
void migrateFromV68(Session& session)
|
||||||
|
{
|
||||||
|
// Changed the way we ref images from release and artists (several releases and artist can now share the same image)
|
||||||
|
session.getDboSession()->execute(R"(CREATE TABLE "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,
|
||||||
|
"image_id" bigint,
|
||||||
|
constraint "fk_release_image" foreign key ("image_id") references "image" ("id") on delete set null deferrable initially deferred))");
|
||||||
|
|
||||||
|
// Migrate data, with the new image_id field set to null
|
||||||
|
session.getDboSession()->execute(R"(INSERT INTO release_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
sort_name,
|
||||||
|
mbid,
|
||||||
|
group_mbid,
|
||||||
|
total_disc,
|
||||||
|
artist_display_name,
|
||||||
|
is_compilation,
|
||||||
|
NULL
|
||||||
|
FROM release
|
||||||
|
)");
|
||||||
|
session.getDboSession()->execute("DROP TABLE release");
|
||||||
|
session.getDboSession()->execute("ALTER TABLE release_backup RENAME TO release");
|
||||||
|
|
||||||
|
session.getDboSession()->execute(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,
|
||||||
|
"image_id" bigint,
|
||||||
|
constraint "fk_artist_image" foreign key ("image_id") references "image" ("id") on delete set null deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
// Migrate data, with the new image_id field set to null
|
||||||
|
session.getDboSession()->execute(R"(INSERT INTO artist_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
sort_name,
|
||||||
|
mbid,
|
||||||
|
NULL
|
||||||
|
FROM artist
|
||||||
|
)");
|
||||||
|
|
||||||
|
session.getDboSession()->execute("DROP TABLE artist");
|
||||||
|
session.getDboSession()->execute("ALTER TABLE artist_backup RENAME TO artist");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
void migrateFromV69(Session& session)
|
||||||
|
{
|
||||||
|
// Add a field in UI settings
|
||||||
|
session.getDboSession()->execute("ALTER TABLE user ADD COLUMN ui_artist_release_sort_method NOT NULL DEFAULT 7"); // 7 = ReleaseSortMethod::OriginalDateDesc
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
|
|
||||||
ScopedNoForeignKeys noPragmaKeys{ session.getDb() };
|
ScopedNoForeignKeys noPragmaKeys{ session.getDb() };
|
||||||
|
|
||||||
@@ -804,6 +921,9 @@ SELECT
|
|||||||
{ 64, migrateFromV64 },
|
{ 64, migrateFromV64 },
|
||||||
{ 65, migrateFromV65 },
|
{ 65, migrateFromV65 },
|
||||||
{ 66, migrateFromV66 },
|
{ 66, migrateFromV66 },
|
||||||
|
{ 67, migrateFromV67 },
|
||||||
|
{ 68, migrateFromV68 },
|
||||||
|
{ 69, migrateFromV69 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
@@ -242,6 +244,11 @@ namespace lms::db
|
|||||||
return session.getDboSession()->add(std::unique_ptr<Label>{ new Label{ name } });
|
return session.getDboSession()->add(std::unique_ptr<Label>{ new Label{ name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t Label::getCount(Session& session)
|
||||||
|
{
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM label"));
|
||||||
|
}
|
||||||
|
|
||||||
Label::pointer Label::find(Session& session, LabelId id)
|
Label::pointer Label::find(Session& session, LabelId id)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
@@ -259,6 +266,15 @@ namespace lms::db
|
|||||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.name = ?").bind(name));
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.name = ?").bind(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RangeResults<LabelId> Label::findOrphanIds(Session& session, std::optional<Range> range)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
// select the labels that have no releases
|
||||||
|
auto query{ session.getDboSession()->query<LabelId>("select l.id from label l LEFT OUTER JOIN release_label r_l ON l.id = r_l.label_id WHERE r_l.release_id IS NULL") };
|
||||||
|
return utils::execRangeQuery<LabelId>(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
ReleaseType::ReleaseType(std::string_view name)
|
ReleaseType::ReleaseType(std::string_view name)
|
||||||
: _name{ name }
|
: _name{ name }
|
||||||
{
|
{
|
||||||
@@ -272,6 +288,11 @@ namespace lms::db
|
|||||||
return session.getDboSession()->add(std::unique_ptr<ReleaseType>{ new ReleaseType{ name } });
|
return session.getDboSession()->add(std::unique_ptr<ReleaseType>{ new ReleaseType{ name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t ReleaseType::getCount(Session& session)
|
||||||
|
{
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM release_type"));
|
||||||
|
}
|
||||||
|
|
||||||
ReleaseType::pointer ReleaseType::find(Session& session, ReleaseTypeId id)
|
ReleaseType::pointer ReleaseType::find(Session& session, ReleaseTypeId id)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
@@ -289,6 +310,15 @@ namespace lms::db
|
|||||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RangeResults<ReleaseTypeId> ReleaseType::findOrphanIds(Session& session, std::optional<Range> range)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
// select the release types that have no releases
|
||||||
|
auto query{ session.getDboSession()->query<ReleaseTypeId>("select r_t.id from release_type r_t LEFT OUTER JOIN release_release_type r_r_t ON r_t.id = r_r_t.release_type_id WHERE r_r_t.release_id IS NULL") };
|
||||||
|
return utils::execRangeQuery<ReleaseTypeId>(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
Release::Release(const std::string& name, const std::optional<core::UUID>& MBID)
|
Release::Release(const std::string& name, const std::optional<core::UUID>& MBID)
|
||||||
: _name{ std::string(name, 0, _maxNameLength) }
|
: _name{ std::string(name, 0, _maxNameLength) }
|
||||||
, _MBID{ MBID ? MBID->getAsString() : "" }
|
, _MBID{ MBID ? MBID->getAsString() : "" }
|
||||||
@@ -540,6 +570,11 @@ namespace lms::db
|
|||||||
return utils::fetchQueryResults<Release::pointer>(query);
|
return utils::fetchQueryResults<Release::pointer>(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectPtr<Image> Release::getImage() const
|
||||||
|
{
|
||||||
|
return ObjectPtr<Image>{ _image };
|
||||||
|
}
|
||||||
|
|
||||||
void Release::clearLabels()
|
void Release::clearLabels()
|
||||||
{
|
{
|
||||||
_labels.clear();
|
_labels.clear();
|
||||||
@@ -560,6 +595,11 @@ namespace lms::db
|
|||||||
_releaseTypes.insert(getDboPtr(releaseType));
|
_releaseTypes.insert(getDboPtr(releaseType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Release::setImage(ObjectPtr<Image> image)
|
||||||
|
{
|
||||||
|
_image = getDboPtr(image);
|
||||||
|
}
|
||||||
|
|
||||||
bool Release::hasVariousArtists() const
|
bool Release::hasVariousArtists() const
|
||||||
{
|
{
|
||||||
// TODO optimize
|
// TODO optimize
|
||||||
@@ -665,5 +705,4 @@ namespace lms::db
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ namespace lms::db
|
|||||||
|
|
||||||
auto transaction{ createWriteTransaction() };
|
auto transaction{ createWriteTransaction() };
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||||
@@ -195,11 +196,10 @@ namespace lms::db
|
|||||||
_session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
|
_session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
|
||||||
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)");
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
|
_session.execute("CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem)");
|
_session.execute("CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem COLLATE NOCASE)");
|
||||||
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
|
_session.execute("CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
|
||||||
|
|
||||||
@@ -217,6 +217,7 @@ namespace lms::db
|
|||||||
_session.execute("CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
||||||
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
||||||
|
_session.execute("CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||||
@@ -248,10 +249,9 @@ namespace lms::db
|
|||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_track_artist_idx ON track_artist_link(type, track_id, artist_id)");
|
||||||
|
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ namespace lms::db
|
|||||||
if (params.directory.isValid())
|
if (params.directory.isValid())
|
||||||
query.where("t.directory_id = ?").bind(params.directory);
|
query.where("t.directory_id = ?").bind(params.directory);
|
||||||
|
|
||||||
|
if (params.hasEmbeddedImage.has_value())
|
||||||
|
query.where("t.has_cover = ?").bind(params.hasEmbeddedImage.value());
|
||||||
|
|
||||||
switch (params.sortMethod)
|
switch (params.sortMethod)
|
||||||
{
|
{
|
||||||
case TrackSortMethod::None:
|
case TrackSortMethod::None:
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||||
Wt::Dbo::field(a, _MBID, "mbid");
|
Wt::Dbo::field(a, _MBID, "mbid");
|
||||||
|
|
||||||
Wt::Dbo::hasOne(a, _image, "artist");
|
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ namespace lms::db
|
|||||||
std::string _sortName;
|
std::string _sortName;
|
||||||
std::string _MBID; // Musicbrainz Identifier
|
std::string _MBID; // Musicbrainz Identifier
|
||||||
|
|
||||||
Wt::Dbo::weak_ptr<Image> _image;
|
Wt::Dbo::ptr<Image> _image;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
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
|
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "database/ArtistId.hpp"
|
|
||||||
#include "database/DirectoryId.hpp"
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ImageId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
@@ -35,6 +34,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
class Directory;
|
class Directory;
|
||||||
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
class Image final : public Object<Image, ImageId>
|
class Image final : public Object<Image, ImageId>
|
||||||
@@ -87,7 +87,6 @@ namespace lms::db
|
|||||||
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||||
void setWidth(std::size_t width) { _width = width; }
|
void setWidth(std::size_t width) { _width = width; }
|
||||||
void setHeight(std::size_t height) { _height = height; }
|
void setHeight(std::size_t height) { _height = height; }
|
||||||
void setArtist(const ObjectPtr<Artist>& artist) { _artist = getDboPtr(artist); }
|
|
||||||
void setDirectory(const ObjectPtr<Directory>& directory) { _directory = getDboPtr(directory); }
|
void setDirectory(const ObjectPtr<Directory>& directory) { _directory = getDboPtr(directory); }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
@@ -101,7 +100,8 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _width, "width");
|
Wt::Dbo::field(a, _width, "width");
|
||||||
Wt::Dbo::field(a, _height, "height");
|
Wt::Dbo::field(a, _height, "height");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
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);
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,8 @@ namespace lms::db
|
|||||||
int _width{};
|
int _width{};
|
||||||
int _height{};
|
int _height{};
|
||||||
|
|
||||||
Wt::Dbo::ptr<Artist> _artist;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _artists;
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases;
|
||||||
Wt::Dbo::ptr<Directory> _directory;
|
Wt::Dbo::ptr<Directory> _directory;
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace lms::db
|
|||||||
class Artist;
|
class Artist;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
|
class Image;
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
@@ -56,8 +57,11 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Label() = default;
|
Label() = default;
|
||||||
|
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, LabelId id);
|
static pointer find(Session& session, LabelId id);
|
||||||
static pointer find(Session& session, std::string_view name);
|
static pointer find(Session& session, std::string_view name);
|
||||||
|
static RangeResults<LabelId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
std::string_view getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
@@ -84,8 +88,11 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ReleaseType() = default;
|
ReleaseType() = default;
|
||||||
|
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, ReleaseTypeId id);
|
static pointer find(Session& session, ReleaseTypeId id);
|
||||||
static pointer find(Session& session, std::string_view name);
|
static pointer find(Session& session, std::string_view name);
|
||||||
|
static RangeResults<ReleaseTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
std::string_view getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
@@ -245,6 +252,7 @@ namespace lms::db
|
|||||||
std::vector<std::string> getLabelNames() const;
|
std::vector<std::string> getLabelNames() const;
|
||||||
std::vector<std::string> getReleaseTypeNames() const;
|
std::vector<std::string> getReleaseTypeNames() const;
|
||||||
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
|
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
|
||||||
|
ObjectPtr<Image> getImage() const;
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setName(std::string_view name) { _name = name; }
|
void setName(std::string_view name) { _name = name; }
|
||||||
@@ -258,6 +266,7 @@ namespace lms::db
|
|||||||
void clearReleaseTypes();
|
void clearReleaseTypes();
|
||||||
void addLabel(ObjectPtr<Label> releaseType);
|
void addLabel(ObjectPtr<Label> releaseType);
|
||||||
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
|
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
|
||||||
|
void setImage(ObjectPtr<Image> image);
|
||||||
|
|
||||||
// Get the artists of this release
|
// Get the artists of this release
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||||
@@ -278,6 +287,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _isCompilation, "is_compilation");
|
Wt::Dbo::field(a, _isCompilation, "is_compilation");
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||||
|
|
||||||
|
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
|
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, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
@@ -300,6 +310,7 @@ namespace lms::db
|
|||||||
std::string _artistDisplayName;
|
std::string _artistDisplayName;
|
||||||
bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5
|
bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<Image> _image;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
|
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ namespace lms::db
|
|||||||
std::optional<int> discNumber; // matching this disc number
|
std::optional<int> discNumber; // matching this disc number
|
||||||
MediaLibraryId mediaLibrary; // If set, tracks in this library
|
MediaLibraryId mediaLibrary; // If set, tracks in this library
|
||||||
DirectoryId directory; // if set, tracks in this directory
|
DirectoryId directory; // if set, tracks in this directory
|
||||||
|
std::optional<bool> hasEmbeddedImage; // if set, tracks that have or not embedded images
|
||||||
|
|
||||||
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
||||||
{
|
{
|
||||||
@@ -172,6 +173,11 @@ namespace lms::db
|
|||||||
directory = _directory;
|
directory = _directory;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setHasEmbeddedImage(std::optional<bool> _hasEmbeddedImage)
|
||||||
|
{
|
||||||
|
hasEmbeddedImage = _hasEmbeddedImage;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PathResult
|
struct PathResult
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ namespace lms::db
|
|||||||
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
||||||
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
||||||
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
||||||
|
static inline constexpr ReleaseSortMethod _defaultUIArtistReleaseSortMethod{ ReleaseSortMethod::OriginalDateDesc };
|
||||||
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
||||||
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
|
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
|
||||||
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
|
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
|
||||||
@@ -106,6 +107,7 @@ namespace lms::db
|
|||||||
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
|
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
|
||||||
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
|
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
|
||||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||||
|
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
|
||||||
void clearAuthTokens();
|
void clearAuthTokens();
|
||||||
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
|
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
|
||||||
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
|
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
|
||||||
@@ -120,6 +122,7 @@ namespace lms::db
|
|||||||
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
|
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
|
||||||
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
|
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
|
||||||
UITheme getUITheme() const { return _uiTheme; }
|
UITheme getUITheme() const { return _uiTheme; }
|
||||||
|
ReleaseSortMethod getUIArtistReleaseSortMethod() const { return _uiArtistReleaseSortMethod; }
|
||||||
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
||||||
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
|
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
|
||||||
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
|
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
|
||||||
@@ -138,6 +141,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
|
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
|
||||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||||
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
||||||
|
Wt::Dbo::field(a, _uiArtistReleaseSortMethod, "ui_artist_release_sort_method");
|
||||||
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
|
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
|
||||||
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
|
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
|
||||||
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
||||||
@@ -156,6 +160,7 @@ namespace lms::db
|
|||||||
std::string _passwordHash;
|
std::string _passwordHash;
|
||||||
Wt::WDateTime _lastLogin;
|
Wt::WDateTime _lastLogin;
|
||||||
UITheme _uiTheme{ defaultUITheme };
|
UITheme _uiTheme{ defaultUITheme };
|
||||||
|
ReleaseSortMethod _uiArtistReleaseSortMethod{ _defaultUIArtistReleaseSortMethod };
|
||||||
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
|
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
|
||||||
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
|
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
|
||||||
std::string _listenbrainzToken; // Musicbrainz Identifier
|
std::string _listenbrainzToken; // Musicbrainz Identifier
|
||||||
@@ -172,5 +177,4 @@ namespace lms::db
|
|||||||
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<UIState>> _uiStates;
|
Wt::Dbo::collection<Wt::Dbo::ptr<UIState>> _uiStates;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -19,8 +19,12 @@
|
|||||||
|
|
||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Artist)
|
TEST_F(DatabaseFixture, Artist)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -697,4 +701,28 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Artist_image)
|
||||||
|
{
|
||||||
|
ScopedArtist release{ session, "MyArtist" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_FALSE(release.get()->getImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedImage image{ session, "/myImage" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->setImage(image.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto artistImage(release.get()->getImage());
|
||||||
|
ASSERT_TRUE(artistImage);
|
||||||
|
EXPECT_EQ(artistImage->getId(), image.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
|
|||||||
@@ -80,10 +80,12 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(Artist::getCount(session), 0);
|
EXPECT_EQ(Artist::getCount(session), 0);
|
||||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||||
|
EXPECT_EQ(Label::getCount(session), 0);
|
||||||
EXPECT_EQ(Listen::getCount(session), 0);
|
EXPECT_EQ(Listen::getCount(session), 0);
|
||||||
EXPECT_EQ(Image::getCount(session), 0);
|
EXPECT_EQ(Image::getCount(session), 0);
|
||||||
EXPECT_EQ(MediaLibrary::getCount(session), 0);
|
EXPECT_EQ(MediaLibrary::getCount(session), 0);
|
||||||
EXPECT_EQ(Release::getCount(session), 0);
|
EXPECT_EQ(Release::getCount(session), 0);
|
||||||
|
EXPECT_EQ(ReleaseType::getCount(session), 0);
|
||||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||||
|
|||||||
@@ -19,8 +19,11 @@
|
|||||||
|
|
||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
using ScopedLabel = ScopedEntity<db::Label>;
|
using ScopedLabel = ScopedEntity<db::Label>;
|
||||||
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
|
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
|
||||||
|
|
||||||
@@ -749,6 +752,43 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Label_orphan)
|
||||||
|
{
|
||||||
|
ScopedLabel label{ session, "MyLabel" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto labels{ Label::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(labels.results.size(), 1);
|
||||||
|
EXPECT_EQ(labels.results.front(), label.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->addLabel(label.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto labels{ Label::findOrphanIds(session) };
|
||||||
|
EXPECT_EQ(labels.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->clearLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto labels{ Label::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(labels.results.size(), 1);
|
||||||
|
EXPECT_EQ(labels.results.front(), label.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, ReleaseType)
|
TEST_F(DatabaseFixture, ReleaseType)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -766,6 +806,44 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, ReleaseType_orphan)
|
||||||
|
{
|
||||||
|
// Orphan tests
|
||||||
|
ScopedReleaseType releaseType{ session, "album" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto releaseTypes{ ReleaseType::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(releaseTypes.results.size(), 1);
|
||||||
|
EXPECT_EQ(releaseTypes.results.front(), releaseType.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->addReleaseType(releaseType.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto releaseTypes{ ReleaseType::findOrphanIds(session) };
|
||||||
|
EXPECT_EQ(releaseTypes.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->clearReleaseTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto releaseTypes{ ReleaseType::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(releaseTypes.results.size(), 1);
|
||||||
|
EXPECT_EQ(releaseTypes.results.front(), releaseType.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Release_releaseType)
|
TEST_F(DatabaseFixture, Release_releaseType)
|
||||||
{
|
{
|
||||||
ScopedRelease release{ session, "MyRelease" };
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
@@ -963,4 +1041,28 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(release3->getTrackCount(), 0);
|
EXPECT_EQ(release3->getTrackCount(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Release_image)
|
||||||
|
{
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_FALSE(release.get()->getImage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedImage image{ session, "/myImage" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
release.get().modify()->setImage(image.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto releaseImage(release.get()->getImage());
|
||||||
|
ASSERT_TRUE(releaseImage);
|
||||||
|
EXPECT_EQ(releaseImage->getId(), image.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -198,26 +198,41 @@ namespace lms::metadata
|
|||||||
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
|
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string> artistTag, std::span<const std::string> artistTagDelimiters)
|
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string> artistTag, std::span<const std::string> artistsTag, std::span<const std::string> artistTagDelimiters)
|
||||||
{
|
{
|
||||||
|
std::string artistDisplayName;
|
||||||
|
|
||||||
if (artists.size() == 1)
|
if (artists.size() == 1)
|
||||||
return artists.front().name;
|
artistDisplayName = artists.front().name;
|
||||||
else if (artists.size() > 1)
|
else if (artists.size() > 1)
|
||||||
{
|
{
|
||||||
std::vector<std::string_view> artistNames;
|
std::vector<std::string_view> artistNames;
|
||||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
|
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
|
||||||
|
|
||||||
// Picard use case: if we manage to match all artists in the "artist" tag (considered single-valued), and if it does not contain any custom artist delimiter, we use it as the display name
|
// Picard use case: if we manage to match all artists in the "artist" tag (considered single-valued), and if no custom delimiter was used, we use it as the display name
|
||||||
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
||||||
if (artistTag && !strIsContainingAny(*artistTag, artistTagDelimiters) && strIsMatchingArtistNames(*artistTag, artistNames))
|
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
||||||
return *artistTag;
|
{
|
||||||
else
|
if (artistsTag.size() == artists.size())
|
||||||
return core::stringUtils::joinStrings(artistNames, ", ");
|
{
|
||||||
|
// artists was used
|
||||||
|
if (std::none_of(std::begin(artistsTag), std::cend(artistsTag), [&](std::string_view tag) { return strIsContainingAny(tag, artistTagDelimiters); }))
|
||||||
|
artistDisplayName = *artistTag;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// artist was used
|
||||||
|
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
||||||
|
artistDisplayName = *artistTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistDisplayName.empty())
|
||||||
|
artistDisplayName = core::stringUtils::joinStrings(artistNames, ", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return artistDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
|
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
|
||||||
@@ -339,7 +354,7 @@ namespace lms::metadata
|
|||||||
|
|
||||||
track.medium = getMedium(tagReader);
|
track.medium = getMedium(tagReader);
|
||||||
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
|
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
|
||||||
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _artistTagDelimiters);
|
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), getTagValuesAs<std::string>(tagReader, TagType::Artists, {}), _artistTagDelimiters);
|
||||||
|
|
||||||
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
|
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
|
||||||
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
|
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
|
||||||
@@ -400,7 +415,7 @@ namespace lms::metadata
|
|||||||
release->name = std::move(*releaseName);
|
release->name = std::move(*releaseName);
|
||||||
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or("");
|
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or("");
|
||||||
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
|
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
|
||||||
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _artistTagDelimiters);
|
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), getTagValuesAs<std::string>(tagReader, TagType::AlbumArtists, {}), _artistTagDelimiters);
|
||||||
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
|
||||||
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
|
||||||
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
|
||||||
|
|||||||
@@ -273,6 +273,45 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Parser, customDelimitersNotForDisplayString)
|
||||||
|
{
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ TagType::Artist, { "Artist1 & Artist2" } },
|
||||||
|
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Parser parser;
|
||||||
|
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
|
||||||
|
|
||||||
|
std::unique_ptr<Track> track{ parser.parse(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
|
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||||
|
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Parser, customDelimitersUsedForArtist)
|
||||||
|
{
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ TagType::Artist, { "Artist1 & Artist2" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Parser parser;
|
||||||
|
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
|
||||||
|
|
||||||
|
std::unique_ptr<Track> track{ parser.parse(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
|
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||||
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Parser, noArtistInArtist)
|
TEST(Parser, noArtistInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
@@ -337,6 +376,23 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Parser, multipleArtistsInArtistsWithEndDelimiter)
|
||||||
|
{
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ TagType::Artist, { "Artist1 & (CV. Artist2)" } },
|
||||||
|
{ TagType::Artists, { "Artist1", "Artist2" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track->artists.size(), 2);
|
||||||
|
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||||
|
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||||
|
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Parser, singleArtistInAlbumArtists)
|
TEST(Parser, singleArtistInAlbumArtists)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
add_subdirectory(artwork)
|
||||||
add_subdirectory(auth)
|
add_subdirectory(auth)
|
||||||
add_subdirectory(cover)
|
|
||||||
add_subdirectory(feedback)
|
add_subdirectory(feedback)
|
||||||
add_subdirectory(recommendation)
|
add_subdirectory(recommendation)
|
||||||
add_subdirectory(scanner)
|
add_subdirectory(scanner)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
add_library(lmsartwork SHARED
|
||||||
|
impl/ImageCache.cpp
|
||||||
|
impl/ArtworkService.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(lmsartwork INTERFACE
|
||||||
|
include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(lmsartwork PRIVATE
|
||||||
|
include
|
||||||
|
impl
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(lmsartwork PRIVATE
|
||||||
|
lmsav
|
||||||
|
lmsimage
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(lmsartwork PUBLIC
|
||||||
|
lmsdatabase
|
||||||
|
lmsimage
|
||||||
|
lmscore
|
||||||
|
std::filesystem
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS lmsartwork DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
|
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 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 "ArtworkService.hpp"
|
||||||
|
|
||||||
|
#include "av/IAudioFile.hpp"
|
||||||
|
#include "core/IConfig.hpp"
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "core/Random.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
|
#include "core/Utils.hpp"
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
|
namespace lms::cover
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace image;
|
||||||
|
|
||||||
|
ArtworkService::ArtworkService(db::Db& db,
|
||||||
|
const std::filesystem::path& defaultReleaseCoverSvgPath,
|
||||||
|
const std::filesystem::path& defaultArtistImageSvgPath)
|
||||||
|
: _db{ db }
|
||||||
|
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||||
|
{
|
||||||
|
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||||
|
|
||||||
|
LMS_LOG(COVER, INFO, "Default release cover path = '" << defaultReleaseCoverSvgPath.string() << "'");
|
||||||
|
LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
|
||||||
|
|
||||||
|
_defaultReleaseCover = image::readSvgFile(defaultReleaseCoverSvgPath); // may throw
|
||||||
|
_defaultArtistImage = image::readSvgFile(defaultArtistImageSvgPath); // may throw
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> ArtworkService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const
|
||||||
|
{
|
||||||
|
std::unique_ptr<IEncodedImage> image;
|
||||||
|
|
||||||
|
input.visitAttachedPictures([&](const av::Picture& picture) {
|
||||||
|
if (image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
|
||||||
|
rawImage->resize(width);
|
||||||
|
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||||
|
}
|
||||||
|
catch (const image::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IEncodedImage> ArtworkService::getFromImageFile(const std::filesystem::path& p, ImageSize width) const
|
||||||
|
{
|
||||||
|
std::unique_ptr<IEncodedImage> image;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
|
||||||
|
rawImage->resize(width);
|
||||||
|
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||||
|
}
|
||||||
|
catch (const image::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> ArtworkService::getDefaultReleaseCover()
|
||||||
|
{
|
||||||
|
return _defaultReleaseCover;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> ArtworkService::getDefaultArtistImage()
|
||||||
|
{
|
||||||
|
return _defaultArtistImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtworkService::checkImageFile(const std::filesystem::path& filePath) 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<IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, ImageSize width) const
|
||||||
|
{
|
||||||
|
std::unique_ptr<IEncodedImage> image;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
image = getFromAvMediaFile(*av::parseAudioFile(p), width);
|
||||||
|
}
|
||||||
|
catch (av::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, ImageSize width)
|
||||||
|
{
|
||||||
|
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
||||||
|
if (cover)
|
||||||
|
return cover;
|
||||||
|
|
||||||
|
{
|
||||||
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Track::pointer track{ db::Track::find(session, trackId) };
|
||||||
|
if (track && track->hasCover())
|
||||||
|
cover = getTrackImage(track->getAbsoluteFilePath(), width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cover)
|
||||||
|
_cache.addImage(cacheEntryDesc, cover);
|
||||||
|
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> ArtworkService::getReleaseCover(db::ReleaseId releaseId, ImageSize width)
|
||||||
|
{
|
||||||
|
using namespace db;
|
||||||
|
const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width };
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
|
||||||
|
if (image)
|
||||||
|
return image;
|
||||||
|
|
||||||
|
{
|
||||||
|
Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Release::pointer release{ db::Release::find(session, releaseId) };
|
||||||
|
if (release)
|
||||||
|
{
|
||||||
|
if (const db::Image::pointer dbImage{ release->getImage() })
|
||||||
|
image = getFromImageFile(dbImage->getAbsoluteFilePath(), width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image)
|
||||||
|
_cache.addImage(cacheEntryDesc, image);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> ArtworkService::getArtistImage(db::ArtistId artistId, ImageSize width)
|
||||||
|
{
|
||||||
|
using namespace db;
|
||||||
|
const ImageCache::EntryDesc cacheEntryDesc{ artistId, width };
|
||||||
|
|
||||||
|
std::shared_ptr<IEncodedImage> artistImage{ _cache.getImage(cacheEntryDesc) };
|
||||||
|
if (artistImage)
|
||||||
|
return artistImage;
|
||||||
|
|
||||||
|
{
|
||||||
|
Session& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
if (const Artist::pointer artist{ Artist::find(session, artistId) })
|
||||||
|
{
|
||||||
|
if (const db::Image::pointer image{ artist->getImage() })
|
||||||
|
artistImage = getFromImageFile(image->getAbsoluteFilePath(), width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistImage)
|
||||||
|
_cache.addImage(cacheEntryDesc, artistImage);
|
||||||
|
|
||||||
|
return artistImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtworkService::flushCache()
|
||||||
|
{
|
||||||
|
_cache.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtworkService::setJpegQuality(unsigned quality)
|
||||||
|
{
|
||||||
|
_jpegQuality = core::utils::clamp<unsigned>(quality, 1, 100);
|
||||||
|
|
||||||
|
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::cover
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 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 <vector>
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
#include "image/IEncodedImage.hpp"
|
||||||
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
|
#include "ImageCache.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace lms::av
|
||||||
|
{
|
||||||
|
class IAudioFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace lms::cover
|
||||||
|
{
|
||||||
|
class ArtworkService : public IArtworkService
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ArtworkService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ArtworkService(const ArtworkService&) = delete;
|
||||||
|
ArtworkService& operator=(const ArtworkService&) = delete;
|
||||||
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, image::ImageSize width) override;
|
||||||
|
std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, image::ImageSize width) override;
|
||||||
|
std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, image::ImageSize width) override;
|
||||||
|
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
|
||||||
|
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
|
||||||
|
|
||||||
|
void flushCache() override;
|
||||||
|
void setJpegQuality(unsigned quality) override;
|
||||||
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> getTrackImage(db::Session& dbSession, db::TrackId trackId, image::ImageSize width, bool allowReleaseFallback);
|
||||||
|
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, image::ImageSize width) const;
|
||||||
|
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, image::ImageSize width) const;
|
||||||
|
|
||||||
|
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, image::ImageSize width) const;
|
||||||
|
|
||||||
|
bool checkImageFile(const std::filesystem::path& filePath) const;
|
||||||
|
|
||||||
|
db::Db& _db;
|
||||||
|
|
||||||
|
ImageCache _cache;
|
||||||
|
std::shared_ptr<image::IEncodedImage> _defaultReleaseCover;
|
||||||
|
std::shared_ptr<image::IEncodedImage> _defaultArtistImage;
|
||||||
|
|
||||||
|
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
|
||||||
|
unsigned _jpegQuality;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lms::cover
|
||||||
+13
-7
@@ -34,22 +34,28 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::cover
|
namespace lms::cover
|
||||||
{
|
{
|
||||||
class ICoverService
|
class IArtworkService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~ICoverService() = default;
|
virtual ~IArtworkService() = default;
|
||||||
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) = 0;
|
virtual std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, image::ImageSize width) = 0;
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) = 0;
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) = 0;
|
|
||||||
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() = 0;
|
// no logic to fallback to release here
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, image::ImageSize width) = 0;
|
||||||
|
|
||||||
|
// no logic to fallback to track here
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, image::ImageSize width) = 0;
|
||||||
|
|
||||||
|
// Svg images dont have image "size"
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() = 0;
|
||||||
|
|
||||||
virtual void flushCache() = 0;
|
virtual void flushCache() = 0;
|
||||||
|
|
||||||
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath);
|
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
|
|
||||||
} // namespace lms::cover
|
} // namespace lms::cover
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
|
|
||||||
add_library(lmsservice-cover SHARED
|
|
||||||
impl/ImageCache.cpp
|
|
||||||
impl/CoverService.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(lmsservice-cover INTERFACE
|
|
||||||
include
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(lmsservice-cover PRIVATE
|
|
||||||
include
|
|
||||||
impl
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(lmsservice-cover PRIVATE
|
|
||||||
lmsav
|
|
||||||
lmsimage
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(lmsservice-cover PUBLIC
|
|
||||||
lmsdatabase
|
|
||||||
lmsimage
|
|
||||||
lmscore
|
|
||||||
std::filesystem
|
|
||||||
)
|
|
||||||
|
|
||||||
install(TARGETS lmsservice-cover DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
|
||||||
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2013 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 "CoverService.hpp"
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
|
||||||
#include "core/IConfig.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "core/Path.hpp"
|
|
||||||
#include "core/Random.hpp"
|
|
||||||
#include "core/String.hpp"
|
|
||||||
#include "core/Utils.hpp"
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Db.hpp"
|
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "image/Exception.hpp"
|
|
||||||
#include "image/Image.hpp"
|
|
||||||
|
|
||||||
namespace lms::cover
|
|
||||||
{
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
struct TrackInfo
|
|
||||||
{
|
|
||||||
bool hasCover{};
|
|
||||||
bool isMultiDisc{};
|
|
||||||
std::filesystem::path trackPath;
|
|
||||||
std::optional<db::ReleaseId> releaseId;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::optional<TrackInfo> getTrackInfo(db::Session& dbSession, db::TrackId trackId)
|
|
||||||
{
|
|
||||||
std::optional<TrackInfo> res;
|
|
||||||
|
|
||||||
auto transaction{ dbSession.createReadTransaction() };
|
|
||||||
|
|
||||||
const db::Track::pointer track{ db::Track::find(dbSession, trackId) };
|
|
||||||
if (!track)
|
|
||||||
return res;
|
|
||||||
|
|
||||||
res = TrackInfo{};
|
|
||||||
|
|
||||||
res->hasCover = track->hasCover();
|
|
||||||
res->trackPath = track->getAbsoluteFilePath();
|
|
||||||
|
|
||||||
if (const db::Release::pointer & release{ track->getRelease() })
|
|
||||||
{
|
|
||||||
res->releaseId = release->getId();
|
|
||||||
if (release->getTotalDisc() > 1)
|
|
||||||
res->isMultiDisc = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> constructPreferredFileNames()
|
|
||||||
{
|
|
||||||
std::vector<std::string> res;
|
|
||||||
|
|
||||||
core::Service<core::IConfig>::get()->visitStrings("cover-preferred-file-names",
|
|
||||||
[&res](std::string_view fileName) {
|
|
||||||
res.emplace_back(fileName);
|
|
||||||
},
|
|
||||||
{ "cover", "front" });
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath)
|
|
||||||
{
|
|
||||||
return std::make_unique<CoverService>(db, defaultSvgCoverPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace image;
|
|
||||||
|
|
||||||
CoverService::CoverService(db::Db& db,
|
|
||||||
const std::filesystem::path& defaultSvgCoverPath)
|
|
||||||
: _db{ db }
|
|
||||||
, _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
|
||||||
, _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
|
|
||||||
, _preferredFileNames{ constructPreferredFileNames() }
|
|
||||||
{
|
|
||||||
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
|
||||||
|
|
||||||
LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'");
|
|
||||||
LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
|
|
||||||
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
|
|
||||||
LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ","));
|
|
||||||
|
|
||||||
_defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const
|
|
||||||
{
|
|
||||||
std::unique_ptr<IEncodedImage> image;
|
|
||||||
|
|
||||||
input.visitAttachedPictures([&](const av::Picture& picture) {
|
|
||||||
if (image)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(picture.data, picture.dataSize) };
|
|
||||||
rawImage->resize(width);
|
|
||||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
|
||||||
}
|
|
||||||
catch (const image::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
|
||||||
{
|
|
||||||
std::unique_ptr<IEncodedImage> image;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::unique_ptr<IRawImage> rawImage{ decodeImage(p) };
|
|
||||||
rawImage->resize(width);
|
|
||||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
|
||||||
}
|
|
||||||
catch (const image::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> CoverService::getDefaultSvgCover()
|
|
||||||
{
|
|
||||||
return _defaultCover;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const
|
|
||||||
{
|
|
||||||
const std::multimap<std::string, std::filesystem::path> coverPaths{ getCoverPaths(directory) };
|
|
||||||
|
|
||||||
auto tryLoadImageFromFilename = [&](std::string_view fileName) {
|
|
||||||
std::unique_ptr<IEncodedImage> image;
|
|
||||||
|
|
||||||
auto range{ coverPaths.equal_range(std::string{ fileName }) };
|
|
||||||
for (auto it{ range.first }; it != range.second; ++it)
|
|
||||||
{
|
|
||||||
image = getFromCoverFile(it->second, width);
|
|
||||||
if (image)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return image;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> image;
|
|
||||||
|
|
||||||
for (const std::string_view filename : preferredFileNames)
|
|
||||||
{
|
|
||||||
image = tryLoadImageFromFilename(filename);
|
|
||||||
if (image)
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowPickRandom)
|
|
||||||
{
|
|
||||||
for (const auto& [filename, coverPath] : coverPaths)
|
|
||||||
{
|
|
||||||
image = getFromCoverFile(coverPath, width);
|
|
||||||
if (image)
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
|
||||||
{
|
|
||||||
std::unique_ptr<IEncodedImage> res;
|
|
||||||
|
|
||||||
std::filesystem::path coverPath{ filePath };
|
|
||||||
for (const std::filesystem::path& extension : _fileExtensions)
|
|
||||||
{
|
|
||||||
coverPath.replace_extension(extension);
|
|
||||||
|
|
||||||
if (!checkCoverFile(coverPath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
res = getFromCoverFile(coverPath, width);
|
|
||||||
if (res)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CoverService::checkCoverFile(const std::filesystem::path& filePath) 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;
|
|
||||||
|
|
||||||
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
|
||||||
{
|
|
||||||
LMS_LOG(COVER, INFO, "Image file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::multimap<std::string, std::filesystem::path> CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
|
|
||||||
{
|
|
||||||
std::multimap<std::string, std::filesystem::path> res;
|
|
||||||
std::error_code ec;
|
|
||||||
|
|
||||||
std::filesystem::directory_iterator itPath(directoryPath, ec);
|
|
||||||
const std::filesystem::directory_iterator itEnd;
|
|
||||||
while (!ec && itPath != itEnd)
|
|
||||||
{
|
|
||||||
const std::filesystem::path& path{ *itPath };
|
|
||||||
|
|
||||||
if (checkCoverFile(path))
|
|
||||||
res.emplace(std::filesystem::path{ path }.filename().replace_extension("").string(), path);
|
|
||||||
|
|
||||||
itPath.increment(ec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IEncodedImage> CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
|
||||||
{
|
|
||||||
std::unique_ptr<IEncodedImage> image;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
image = getFromAvMediaFile(*av::parseAudioFile(p), width);
|
|
||||||
}
|
|
||||||
catch (av::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(db::TrackId trackId, ImageSize width)
|
|
||||||
{
|
|
||||||
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> CoverService::getFromTrack(db::Session& dbSession, db::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
|
||||||
{
|
|
||||||
using namespace db;
|
|
||||||
|
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (cover)
|
|
||||||
return cover;
|
|
||||||
|
|
||||||
if (const std::optional<TrackInfo> trackInfo{ getTrackInfo(dbSession, trackId) })
|
|
||||||
{
|
|
||||||
if (trackInfo->hasCover)
|
|
||||||
cover = getFromTrack(trackInfo->trackPath, width);
|
|
||||||
|
|
||||||
if (!cover)
|
|
||||||
cover = getFromSameNamedFile(trackInfo->trackPath, width);
|
|
||||||
|
|
||||||
if (!cover && trackInfo->releaseId && allowReleaseFallback)
|
|
||||||
cover = getFromRelease(*trackInfo->releaseId, width);
|
|
||||||
|
|
||||||
if (!cover && trackInfo->isMultiDisc)
|
|
||||||
{
|
|
||||||
if (trackInfo->trackPath.parent_path().has_parent_path())
|
|
||||||
cover = getFromDirectory(trackInfo->trackPath.parent_path().parent_path(), width, _preferredFileNames, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cover)
|
|
||||||
_cache.addImage(cacheEntryDesc, cover);
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width)
|
|
||||||
{
|
|
||||||
using namespace db;
|
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (cover)
|
|
||||||
return cover;
|
|
||||||
|
|
||||||
struct ReleaseInfo
|
|
||||||
{
|
|
||||||
TrackId firstTrackId;
|
|
||||||
std::filesystem::path releaseDirectory;
|
|
||||||
};
|
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
|
||||||
|
|
||||||
auto getReleaseInfo{ [&] {
|
|
||||||
std::optional<ReleaseInfo> res;
|
|
||||||
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
// get a track in this release, consider the release is in a single directory
|
|
||||||
const auto tracks{ Track::find(session, Track::FindParameters{}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) };
|
|
||||||
if (!tracks.results.empty())
|
|
||||||
{
|
|
||||||
const Track::pointer& track{ tracks.results.front() };
|
|
||||||
res = ReleaseInfo{};
|
|
||||||
res->firstTrackId = track->getId();
|
|
||||||
res->releaseDirectory = track->getAbsoluteFilePath().parent_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} };
|
|
||||||
|
|
||||||
if (const std::optional<ReleaseInfo> releaseInfo{ getReleaseInfo() })
|
|
||||||
{
|
|
||||||
cover = getFromDirectory(releaseInfo->releaseDirectory, width, _preferredFileNames, true);
|
|
||||||
if (!cover)
|
|
||||||
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cover)
|
|
||||||
_cache.addImage(cacheEntryDesc, cover);
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> CoverService::getFromArtist(db::ArtistId artistId, ImageSize width)
|
|
||||||
{
|
|
||||||
using namespace db;
|
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ artistId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<IEncodedImage> artistImage{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (artistImage)
|
|
||||||
return artistImage;
|
|
||||||
|
|
||||||
{
|
|
||||||
Session& session{ _db.getTLSSession() };
|
|
||||||
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
if (const Artist::pointer artist{ Artist::find(session, artistId) })
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ artist->getImage() })
|
|
||||||
artistImage = getFromCoverFile(image->getAbsoluteFilePath(), width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (artistImage)
|
|
||||||
_cache.addImage(cacheEntryDesc, artistImage);
|
|
||||||
|
|
||||||
return artistImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CoverService::flushCache()
|
|
||||||
{
|
|
||||||
_cache.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CoverService::setJpegQuality(unsigned quality)
|
|
||||||
{
|
|
||||||
_jpegQuality = core::utils::clamp<unsigned>(quality, 1, 100);
|
|
||||||
|
|
||||||
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace lms::cover
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2015 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 <map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
#include "image/IEncodedImage.hpp"
|
|
||||||
#include "services/cover/ICoverService.hpp"
|
|
||||||
|
|
||||||
#include "ImageCache.hpp"
|
|
||||||
|
|
||||||
namespace lms::db
|
|
||||||
{
|
|
||||||
class Session;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace lms::av
|
|
||||||
{
|
|
||||||
class IAudioFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace lms::cover
|
|
||||||
{
|
|
||||||
class CoverService : public ICoverService
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath);
|
|
||||||
|
|
||||||
private:
|
|
||||||
CoverService(const CoverService&) = delete;
|
|
||||||
CoverService& operator=(const CoverService&) = delete;
|
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) override;
|
|
||||||
std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override;
|
|
||||||
std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) override;
|
|
||||||
std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() override;
|
|
||||||
void flushCache() override;
|
|
||||||
void setJpegQuality(unsigned quality) override;
|
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> getFromTrack(db::Session& dbSession, db::TrackId trackId, image::ImageSize width, bool allowReleaseFallback);
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromAvMediaFile(const av::IAudioFile& input, image::ImageSize width) const;
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, image::ImageSize width) const;
|
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromTrack(const std::filesystem::path& path, image::ImageSize width) const;
|
|
||||||
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const;
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const;
|
|
||||||
|
|
||||||
bool checkCoverFile(const std::filesystem::path& filePath) const;
|
|
||||||
|
|
||||||
db::Db& _db;
|
|
||||||
|
|
||||||
ImageCache _cache;
|
|
||||||
std::shared_ptr<image::IEncodedImage> _defaultCover;
|
|
||||||
|
|
||||||
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
|
|
||||||
const std::size_t _maxFileSize;
|
|
||||||
const std::vector<std::string> _preferredFileNames;
|
|
||||||
unsigned _jpegQuality;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace lms::cover
|
|
||||||
@@ -4,6 +4,7 @@ add_library(lmsscanner SHARED
|
|||||||
impl/ScannerService.cpp
|
impl/ScannerService.cpp
|
||||||
impl/ScannerStats.cpp
|
impl/ScannerStats.cpp
|
||||||
impl/ScanStepAssociateArtistImages.cpp
|
impl/ScanStepAssociateArtistImages.cpp
|
||||||
|
impl/ScanStepAssociateReleaseImages.cpp
|
||||||
impl/ScanStepCheckForDuplicatedFiles.cpp
|
impl/ScanStepCheckForDuplicatedFiles.cpp
|
||||||
impl/ScanStepCheckForRemovedFiles.cpp
|
impl/ScanStepCheckForRemovedFiles.cpp
|
||||||
impl/ScanStepCompact.cpp
|
impl/ScanStepCompact.cpp
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace lms::scanner
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::size_t readBatchSize{ 100 };
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
constexpr std::size_t writeBatchSize{ 10 };
|
constexpr std::size_t writeBatchSize{ 20 };
|
||||||
|
|
||||||
struct ArtistImageAssociation
|
struct ArtistImageAssociation
|
||||||
{
|
{
|
||||||
@@ -170,15 +170,15 @@ namespace lms::scanner
|
|||||||
|
|
||||||
void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations)
|
void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations)
|
||||||
{
|
{
|
||||||
if (imageAssociations.empty())
|
while (!imageAssociations.empty())
|
||||||
return;
|
|
||||||
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
|
|
||||||
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
|
||||||
{
|
{
|
||||||
updateArtistImage(session, imageAssociations.front());
|
auto transaction{ session.createWriteTransaction() };
|
||||||
imageAssociations.pop_front();
|
|
||||||
|
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
|
{
|
||||||
|
updateArtistImage(session, imageAssociations.front());
|
||||||
|
imageAssociations.pop_front();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ScanStepAssociateReleaseImages.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cassert>
|
||||||
|
#include <deque>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include "core/IConfig.hpp"
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "image/Exception.hpp"
|
||||||
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
|
constexpr std::size_t writeBatchSize{ 20 };
|
||||||
|
|
||||||
|
struct ReleaseImageAssociation
|
||||||
|
{
|
||||||
|
db::ReleaseId releaseId;
|
||||||
|
db::ImageId imageId;
|
||||||
|
};
|
||||||
|
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
|
||||||
|
|
||||||
|
struct SearchImageContext
|
||||||
|
{
|
||||||
|
db::Session& session;
|
||||||
|
db::ReleaseId lastRetrievedReleaseId;
|
||||||
|
const std::vector<std::string>& releaseFileNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath)
|
||||||
|
{
|
||||||
|
db::Image::pointer image;
|
||||||
|
|
||||||
|
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
|
||||||
|
if (directory) // may not exist for releases that are split on different media libraries
|
||||||
|
{
|
||||||
|
for (std::string_view fileStem : searchContext.releaseFileNames)
|
||||||
|
{
|
||||||
|
db::Image::FindParameters params;
|
||||||
|
params.setDirectory(directory->getId());
|
||||||
|
params.setFileStem(fileStem);
|
||||||
|
|
||||||
|
db::Image::find(searchContext.session, params, [&](const db::Image::pointer foundImg) {
|
||||||
|
if (!image)
|
||||||
|
image = foundImg;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (image)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::Image::pointer computeBestReleaseImage(SearchImageContext& searchContext, const db::Release::pointer& release)
|
||||||
|
{
|
||||||
|
db::Image::pointer image;
|
||||||
|
|
||||||
|
const auto mbid{ release->getMBID() };
|
||||||
|
if (mbid)
|
||||||
|
{
|
||||||
|
// Find anywhere, since it is suppoed to be unique!
|
||||||
|
db::Image::find(searchContext.session, db::Image::FindParameters{}.setFileStem(mbid->getAsString()), [&](const db::Image::pointer foundImg) {
|
||||||
|
if (!image)
|
||||||
|
image = foundImg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!image)
|
||||||
|
{
|
||||||
|
std::set<std::filesystem::path> releasePaths;
|
||||||
|
db::Directory::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
|
||||||
|
db::Directory::find(searchContext.session, params, [&](const db::Directory::pointer& directory) {
|
||||||
|
releasePaths.insert(directory->getAbsolutePath());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expect layout like this:
|
||||||
|
// Artist/Release/CD1/...
|
||||||
|
// /CD2/...
|
||||||
|
// /cover.jpg
|
||||||
|
if (releasePaths.size() > 1)
|
||||||
|
{
|
||||||
|
const std::filesystem::path releasePath{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
|
||||||
|
image = findImageInDirectory(searchContext, releasePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!image)
|
||||||
|
{
|
||||||
|
for (const std::filesystem::path& releasePath : releasePaths)
|
||||||
|
{
|
||||||
|
image = findImageInDirectory(searchContext, releasePath);
|
||||||
|
if (image)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchNextReleaseImagesToUpdate(SearchImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
||||||
|
{
|
||||||
|
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
|
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
|
||||||
|
db::Image::pointer image{ computeBestReleaseImage(searchContext, release) };
|
||||||
|
|
||||||
|
if (image != release->getImage())
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updating release image for release '" << release->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "<none>") << "'");
|
||||||
|
releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), image ? image->getId() : db::ImageId{} });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return releaseId != searchContext.lastRetrievedReleaseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateReleaseImage(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation)
|
||||||
|
{
|
||||||
|
db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) };
|
||||||
|
assert(release);
|
||||||
|
|
||||||
|
db::Image::pointer image;
|
||||||
|
if (releaseImageAssociation.imageId.isValid())
|
||||||
|
image = db::Image::find(session, releaseImageAssociation.imageId);
|
||||||
|
|
||||||
|
release.modify()->setImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
|
||||||
|
{
|
||||||
|
while (!imageAssociations.empty())
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
|
{
|
||||||
|
updateReleaseImage(session, imageAssociations.front());
|
||||||
|
imageAssociations.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> constructReleaseFileNames()
|
||||||
|
{
|
||||||
|
std::vector<std::string> res;
|
||||||
|
|
||||||
|
core::Service<core::IConfig>::get()->visitStrings("cover-preferred-file-names",
|
||||||
|
[&res](std::string_view fileName) {
|
||||||
|
res.emplace_back(fileName);
|
||||||
|
},
|
||||||
|
{ "cover", "front", "folder", "default" });
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams)
|
||||||
|
: ScanStepBase{ initParams }
|
||||||
|
, _releaseFileNames{ constructReleaseFileNames() }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepAssociateReleaseImages::process(ScanContext& context)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (context.stats.nbChanges() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
context.currentStepStats.totalElems = db::Release::getCount(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchImageContext searchContext{
|
||||||
|
.session = session,
|
||||||
|
.lastRetrievedReleaseId = {},
|
||||||
|
.releaseFileNames = _releaseFileNames,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReleaseImageAssociationContainer releaseImageAssociations;
|
||||||
|
while (fetchNextReleaseImagesToUpdate(searchContext, releaseImageAssociations))
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
updateReleaseImages(session, releaseImageAssociations);
|
||||||
|
context.currentStepStats.processedElems += readBatchSize;
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanStepAssociateReleaseImages : public ScanStepBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScanStepAssociateReleaseImages(InitParams& initParams);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
|
||||||
|
core::LiteralString getStepName() const override { return "Associate release images"; }
|
||||||
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
|
const std::vector<std::string> _releaseFileNames;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -37,6 +37,8 @@ namespace lms::scanner
|
|||||||
removeOrphanedClusterTypes(context);
|
removeOrphanedClusterTypes(context);
|
||||||
removeOrphanedArtists(context);
|
removeOrphanedArtists(context);
|
||||||
removeOrphanedReleases(context);
|
removeOrphanedReleases(context);
|
||||||
|
removeOrphanedReleaseTypes(context);
|
||||||
|
removeOrphanedLabels(context);
|
||||||
removeOrphanedDirectories(context);
|
removeOrphanedDirectories(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,18 @@ namespace lms::scanner
|
|||||||
removeOrphanedEntries<db::Release>(context);
|
removeOrphanedEntries<db::Release>(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedReleaseTypes(ScanContext& context)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned release types...");
|
||||||
|
removeOrphanedEntries<db::ReleaseType>(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedLabels(ScanContext& context)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned labels...");
|
||||||
|
removeOrphanedEntries<db::Label>(context);
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedDirectories(ScanContext& context)
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedDirectories(ScanContext& context)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned directories...");
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned directories...");
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ namespace lms::scanner
|
|||||||
void removeOrphanedClusterTypes(ScanContext& context);
|
void removeOrphanedClusterTypes(ScanContext& context);
|
||||||
void removeOrphanedArtists(ScanContext& context);
|
void removeOrphanedArtists(ScanContext& context);
|
||||||
void removeOrphanedReleases(ScanContext& context);
|
void removeOrphanedReleases(ScanContext& context);
|
||||||
|
void removeOrphanedReleaseTypes(ScanContext& context);
|
||||||
|
void removeOrphanedLabels(ScanContext& context);
|
||||||
void removeOrphanedDirectories(ScanContext& context);
|
void removeOrphanedDirectories(ScanContext& context);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
|
|||||||
@@ -252,7 +252,17 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Release::pointer getOrCreateRelease(Session& session, std::optional<std::size_t> mediumPosition, const metadata::Release& releaseInfo, const Directory::pointer& currentDirectory)
|
// Compare release level info
|
||||||
|
bool isReleaseMatching(const Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||||
|
{
|
||||||
|
// TODO: add more criterias?
|
||||||
|
return candidateRelease->getName() == releaseInfo.name
|
||||||
|
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||||
|
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||||
|
&& candidateRelease->isCompilation() == releaseInfo.isCompilation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Release::pointer getOrCreateRelease(Session& session, const metadata::Release& releaseInfo, const Directory::pointer& currentDirectory)
|
||||||
{
|
{
|
||||||
Release::pointer release;
|
Release::pointer release;
|
||||||
|
|
||||||
@@ -265,12 +275,36 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
else if (releaseInfo.name.empty())
|
else if (releaseInfo.name.empty())
|
||||||
{
|
{
|
||||||
// No release name (only mbid) -> nothing to de
|
// No release name (only mbid) -> nothing to do
|
||||||
return release;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back on release name (collisions may occur)
|
// Fall back on release name (collisions may occur)
|
||||||
// First try in the current directory
|
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||||
|
const DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||||
|
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||||
|
{
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setParentDirectory(parentDirectoryId);
|
||||||
|
params.setName(releaseInfo.name);
|
||||||
|
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
|
||||||
|
// Already found a candidate
|
||||||
|
if (release)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Do not fallback on properly tagged releases
|
||||||
|
if (candidateRelease->getMBID().has_value())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||||
|
return;
|
||||||
|
|
||||||
|
release = candidateRelease;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lastly try in the current directory: we do this at last to have
|
||||||
|
// opportunities to merge releases in case of migration / rescan
|
||||||
if (!release)
|
if (!release)
|
||||||
{
|
{
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
@@ -285,30 +319,7 @@ namespace lms::scanner
|
|||||||
if (candidateRelease->getMBID().has_value())
|
if (candidateRelease->getMBID().has_value())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// TODO: add more criterias?
|
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||||
release = candidateRelease;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// second try in another sibling directory (case for Album/DiscX)
|
|
||||||
const DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
|
||||||
if (!release && mediumPosition && parentDirectoryId.isValid())
|
|
||||||
{
|
|
||||||
Release::FindParameters params;
|
|
||||||
params.setParentDirectory(parentDirectoryId);
|
|
||||||
params.setName(releaseInfo.name);
|
|
||||||
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
|
|
||||||
// Already found a candidate
|
|
||||||
if (release)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Do not fallback on properly tagged releases
|
|
||||||
if (candidateRelease->getMBID().has_value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Fallback only if the disc number of the current track is not the same
|
|
||||||
const std::vector<DiscInfo> discs{ candidateRelease->getDiscs() };
|
|
||||||
if (discs.empty() || std::any_of(discs.begin(), discs.end(), [&](const DiscInfo& discInfo) { return discInfo.position == *mediumPosition; }))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
release = candidateRelease;
|
release = candidateRelease;
|
||||||
@@ -737,7 +748,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
track.modify()->setScanVersion(_settings.scanVersion);
|
track.modify()->setScanVersion(_settings.scanVersion);
|
||||||
if (trackMetadata->medium && trackMetadata->medium->release)
|
if (trackMetadata->medium && trackMetadata->medium->release)
|
||||||
track.modify()->setRelease(getOrCreateRelease(dbSession, trackMetadata->medium->position, *trackMetadata->medium->release, directory));
|
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata->medium->release, directory));
|
||||||
else
|
else
|
||||||
track.modify()->setRelease({});
|
track.modify()->setRelease({});
|
||||||
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
|
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
#include "ScanStepAssociateArtistImages.hpp"
|
#include "ScanStepAssociateArtistImages.hpp"
|
||||||
|
#include "ScanStepAssociateReleaseImages.hpp"
|
||||||
#include "ScanStepCheckForDuplicatedFiles.hpp"
|
#include "ScanStepCheckForDuplicatedFiles.hpp"
|
||||||
#include "ScanStepCheckForRemovedFiles.hpp"
|
#include "ScanStepCheckForRemovedFiles.hpp"
|
||||||
#include "ScanStepCompact.hpp"
|
#include "ScanStepCompact.hpp"
|
||||||
@@ -347,6 +348,7 @@ namespace lms::scanner
|
|||||||
_scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
||||||
|
_scanSteps.push_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ namespace lms::scanner
|
|||||||
enum class ScanStep
|
enum class ScanStep
|
||||||
{
|
{
|
||||||
AssociateArtistImages,
|
AssociateArtistImages,
|
||||||
|
AssociateReleaseImages,
|
||||||
CheckForDuplicatedFiles,
|
CheckForDuplicatedFiles,
|
||||||
CheckForRemovedFiles,
|
CheckForRemovedFiles,
|
||||||
ComputeClusterStats,
|
ComputeClusterStats,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ target_link_libraries(lmssubsonic PRIVATE
|
|||||||
lmsrecommendation
|
lmsrecommendation
|
||||||
lmsscanner
|
lmsscanner
|
||||||
lmsscrobbling
|
lmsscrobbling
|
||||||
lmsservice-cover
|
lmsartwork
|
||||||
lmscore
|
lmscore
|
||||||
std::filesystem
|
std::filesystem
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ namespace lms::api::subsonic
|
|||||||
SubsonicResource::SubsonicResource(db::Db& db)
|
SubsonicResource::SubsonicResource(db::Db& db)
|
||||||
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
|
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
|
||||||
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
|
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
|
||||||
, _defaultCoverClients{ readDefaultCoverClients() }
|
, _defaultReleaseCoverClients{ readDefaultCoverClients() }
|
||||||
, _db{ db }
|
, _db{ db }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -410,7 +410,7 @@ namespace lms::api::subsonic
|
|||||||
const ClientInfo clientInfo{ getClientInfo(request) };
|
const ClientInfo clientInfo{ getClientInfo(request) };
|
||||||
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
||||||
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
|
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
|
||||||
bool enableDefaultCover{ _defaultCoverClients.contains(clientInfo.name) };
|
bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) };
|
||||||
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
||||||
|
|
||||||
db::User::pointer user;
|
db::User::pointer user;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||||
const std::unordered_set<std::string> _openSubsonicDisabledClients;
|
const std::unordered_set<std::string> _openSubsonicDisabledClients;
|
||||||
const std::unordered_set<std::string> _defaultCoverClients;
|
const std::unordered_set<std::string> _defaultReleaseCoverClients;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/cover/ICoverService.hpp"
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
#include "ParameterParsing.hpp"
|
#include "ParameterParsing.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
@@ -267,14 +267,14 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> cover;
|
std::shared_ptr<image::IEncodedImage> cover;
|
||||||
if (trackId)
|
if (trackId)
|
||||||
cover = core::Service<cover::ICoverService>::get()->getFromTrack(*trackId, size);
|
cover = core::Service<cover::IArtworkService>::get()->getTrackImage(*trackId, size);
|
||||||
else if (releaseId)
|
else if (releaseId)
|
||||||
cover = core::Service<cover::ICoverService>::get()->getFromRelease(*releaseId, size);
|
cover = core::Service<cover::IArtworkService>::get()->getReleaseCover(*releaseId, size);
|
||||||
else if (artistId)
|
else if (artistId)
|
||||||
cover = core::Service<cover::ICoverService>::get()->getFromArtist(*artistId, size);
|
cover = core::Service<cover::IArtworkService>::get()->getArtistImage(*artistId, size);
|
||||||
|
|
||||||
if (!cover && context.enableDefaultCover && !artistId)
|
if (!cover && context.enableDefaultCover && !artistId)
|
||||||
cover = core::Service<cover::ICoverService>::get()->getDefaultSvgCover();
|
cover = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
|
||||||
|
|
||||||
if (!cover)
|
if (!cover)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
@@ -82,7 +84,21 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
|
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
|
||||||
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
if (release->getImage())
|
||||||
|
{
|
||||||
|
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db::Track::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setHasEmbeddedImage(true);
|
||||||
|
params.setRange(db::Range{ 0, 1 });
|
||||||
|
|
||||||
|
db::Track::find(context.dbSession, params, [&](const db::Track::pointer& track) {
|
||||||
|
albumNode.setAttribute("coverArt", idToString(track->getId()));
|
||||||
|
});
|
||||||
|
}
|
||||||
if (const auto year{ release->getYear() })
|
if (const auto year{ release->getYear() })
|
||||||
albumNode.setAttribute("year", *year);
|
albumNode.setAttribute("year", *year);
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
artistNode.setAttribute("id", idToString(artist->getId()));
|
artistNode.setAttribute("id", idToString(artist->getId()));
|
||||||
artistNode.setAttribute("name", artist->getName());
|
artistNode.setAttribute("name", artist->getName());
|
||||||
if (const db::Image::pointer artistImage{ artist->getImage() })
|
if (artist->getImage())
|
||||||
artistNode.setAttribute("coverArt", idToString(artist->getId()));
|
artistNode.setAttribute("coverArt", idToString(artist->getId()));
|
||||||
|
|
||||||
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
||||||
|
|
||||||
Response::Node createPlaylistNode(const TrackList::pointer& tracklist, Session&)
|
Response::Node createPlaylistNode(const TrackList::pointer& tracklist, Session& session)
|
||||||
{
|
{
|
||||||
Response::Node playlistNode;
|
Response::Node playlistNode;
|
||||||
|
|
||||||
@@ -43,8 +43,15 @@ namespace lms::api::subsonic
|
|||||||
playlistNode.setAttribute("created", reportedDummyDate);
|
playlistNode.setAttribute("created", reportedDummyDate);
|
||||||
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
||||||
|
|
||||||
if (const auto entry{ tracklist->getEntry(0) })
|
db::Track::FindParameters params;
|
||||||
playlistNode.setAttribute("coverArt", idToString(entry->getTrack()->getId()));
|
params.setTrackList(tracklist->getId());
|
||||||
|
params.setHasEmbeddedImage(true);
|
||||||
|
params.setRange(db::Range{ 0, 1 });
|
||||||
|
params.setSortMethod(TrackSortMethod::TrackList);
|
||||||
|
|
||||||
|
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||||
|
playlistNode.setAttribute("coverArt", idToString(track->getId()));
|
||||||
|
});
|
||||||
|
|
||||||
return playlistNode;
|
return playlistNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackArtistLink.hpp"
|
#include "database/TrackArtistLink.hpp"
|
||||||
@@ -110,7 +111,7 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
if (track->hasCover())
|
if (track->hasCover())
|
||||||
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
||||||
else if (release)
|
else if (release && release->getImage())
|
||||||
trackResponse.setAttribute("coverArt", idToString(release->getId()));
|
trackResponse.setAttribute("coverArt", idToString(release->getId()));
|
||||||
|
|
||||||
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ add_executable(lms
|
|||||||
ui/PlayQueue.cpp
|
ui/PlayQueue.cpp
|
||||||
ui/SettingsView.cpp
|
ui/SettingsView.cpp
|
||||||
ui/State.cpp
|
ui/State.cpp
|
||||||
|
ui/Tooltip.cpp
|
||||||
ui/Utils.cpp
|
ui/Utils.cpp
|
||||||
ui/admin/InitWizardView.cpp
|
ui/admin/InitWizardView.cpp
|
||||||
ui/admin/MediaLibrariesView.cpp
|
ui/admin/MediaLibrariesView.cpp
|
||||||
@@ -50,7 +51,7 @@ add_executable(lms
|
|||||||
ui/explore/TracksView.cpp
|
ui/explore/TracksView.cpp
|
||||||
ui/resource/AudioFileResource.cpp
|
ui/resource/AudioFileResource.cpp
|
||||||
ui/resource/AudioTranscodingResource.cpp
|
ui/resource/AudioTranscodingResource.cpp
|
||||||
ui/resource/CoverResource.cpp
|
ui/resource/ArtworkResource.cpp
|
||||||
ui/resource/DownloadResource.cpp
|
ui/resource/DownloadResource.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ target_link_libraries(lms PRIVATE
|
|||||||
lmsrecommendation
|
lmsrecommendation
|
||||||
lmsscanner
|
lmsscanner
|
||||||
lmsscrobbling
|
lmsscrobbling
|
||||||
lmsservice-cover
|
lmsartwork
|
||||||
lmssubsonic
|
lmssubsonic
|
||||||
lmscore
|
lmscore
|
||||||
)
|
)
|
||||||
|
|||||||
+172
-147
@@ -30,14 +30,15 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
#include "core/SystemPaths.hpp"
|
||||||
#include "core/WtLogger.hpp"
|
#include "core/WtLogger.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
#include "services/auth/IAuthTokenService.hpp"
|
#include "services/auth/IAuthTokenService.hpp"
|
||||||
#include "services/auth/IEnvService.hpp"
|
#include "services/auth/IEnvService.hpp"
|
||||||
#include "services/auth/IPasswordService.hpp"
|
#include "services/auth/IPasswordService.hpp"
|
||||||
#include "services/cover/ICoverService.hpp"
|
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
@@ -50,180 +51,204 @@
|
|||||||
|
|
||||||
namespace lms
|
namespace lms
|
||||||
{
|
{
|
||||||
std::size_t getThreadCount()
|
namespace
|
||||||
{
|
{
|
||||||
const unsigned long configHttpServerThreadCount{ core::Service<core::IConfig>::get()->getULong("http-server-thread-count", 0) };
|
std::size_t getThreadCount()
|
||||||
|
|
||||||
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
|
||||||
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
|
||||||
}
|
|
||||||
|
|
||||||
core::logging::Severity getLogMinSeverity()
|
|
||||||
{
|
|
||||||
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
|
|
||||||
|
|
||||||
if (minSeverity == "debug")
|
|
||||||
return core::logging::Severity::DEBUG;
|
|
||||||
else if (minSeverity == "info")
|
|
||||||
return core::logging::Severity::INFO;
|
|
||||||
else if (minSeverity == "warning")
|
|
||||||
return core::logging::Severity::WARNING;
|
|
||||||
else if (minSeverity == "error")
|
|
||||||
return core::logging::Severity::ERROR;
|
|
||||||
else if (minSeverity == "fatal")
|
|
||||||
return core::logging::Severity::FATAL;
|
|
||||||
|
|
||||||
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<core::tracing::Level> getTracingLevel()
|
|
||||||
{
|
|
||||||
std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") };
|
|
||||||
|
|
||||||
if (tracingLevel == "disabled")
|
|
||||||
return std::nullopt;
|
|
||||||
else if (tracingLevel == "overview")
|
|
||||||
return core::tracing::Level::Overview;
|
|
||||||
else if (tracingLevel == "detailed")
|
|
||||||
return core::tracing::Level::Detailed;
|
|
||||||
|
|
||||||
throw core::LmsException{ "Invalid config value for 'tracing-level'" };
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> generateWtConfig(std::string execPath, core::logging::Severity minSeverity)
|
|
||||||
{
|
|
||||||
std::vector<std::string> args;
|
|
||||||
|
|
||||||
const std::filesystem::path wtConfigPath{ core::Service<core::IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
|
|
||||||
const std::filesystem::path wtLogFilePath{ core::Service<core::IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
|
|
||||||
const std::filesystem::path wtAccessLogFilePath{ core::Service<core::IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
|
|
||||||
const std::filesystem::path wtResourcesPath{ core::Service<core::IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
|
|
||||||
|
|
||||||
args.push_back(execPath);
|
|
||||||
args.push_back("--config=" + wtConfigPath.string());
|
|
||||||
args.push_back("--docroot=" + std::string{ core::Service<core::IConfig>::get()->getString("docroot") });
|
|
||||||
args.push_back("--approot=" + std::string{ core::Service<core::IConfig>::get()->getString("approot") });
|
|
||||||
args.push_back("--deploy-path=" + std::string{ core::Service<core::IConfig>::get()->getString("deploy-path", "/") });
|
|
||||||
if (!wtResourcesPath.empty())
|
|
||||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
|
||||||
|
|
||||||
if (core::Service<core::IConfig>::get()->getBool("tls-enable", false))
|
|
||||||
{
|
{
|
||||||
args.push_back("--https-port=" + std::to_string(core::Service<core::IConfig>::get()->getULong("listen-port", 5082)));
|
const unsigned long configHttpServerThreadCount{ core::Service<core::IConfig>::get()->getULong("http-server-thread-count", 0) };
|
||||||
args.push_back("--https-address=" + std::string{ core::Service<core::IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
|
||||||
args.push_back("--ssl-certificate=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-cert") });
|
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
||||||
args.push_back("--ssl-private-key=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-key") });
|
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
||||||
args.push_back("--ssl-tmp-dh=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-dh") });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
args.push_back("--http-port=" + std::to_string(core::Service<core::IConfig>::get()->getULong("listen-port", 5082)));
|
|
||||||
args.push_back("--http-address=" + std::string{ core::Service<core::IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wtAccessLogFilePath.empty())
|
core::logging::Severity getLogMinSeverity()
|
||||||
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
|
||||||
|
|
||||||
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
|
||||||
|
|
||||||
// Generate the wt_config.xml file
|
|
||||||
boost::property_tree::ptree pt;
|
|
||||||
|
|
||||||
pt.put("server.application-settings.<xmlattr>.location", "*");
|
|
||||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
|
||||||
|
|
||||||
// log-config
|
|
||||||
pt.put("server.application-settings.log-config", core::logging::WtLogger::computeLogConfig(minSeverity));
|
|
||||||
pt.put("server.application-settings.behind-reverse-proxy", core::Service<core::IConfig>::get()->getBool("behind-reverse-proxy", false));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree viewport;
|
std::string_view minSeverity{ core::Service<core::IConfig>::get()->getString("log-min-severity", "info") };
|
||||||
viewport.put("<xmlattr>.name", "viewport");
|
|
||||||
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
if (minSeverity == "debug")
|
||||||
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
return core::logging::Severity::DEBUG;
|
||||||
}
|
else if (minSeverity == "info")
|
||||||
{
|
return core::logging::Severity::INFO;
|
||||||
boost::property_tree::ptree themeColor;
|
else if (minSeverity == "warning")
|
||||||
themeColor.put("<xmlattr>.name", "theme-color");
|
return core::logging::Severity::WARNING;
|
||||||
themeColor.put("<xmlattr>.content", "#303030");
|
else if (minSeverity == "error")
|
||||||
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
return core::logging::Severity::ERROR;
|
||||||
|
else if (minSeverity == "fatal")
|
||||||
|
return core::logging::Severity::FATAL;
|
||||||
|
|
||||||
|
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<core::tracing::Level> getTracingLevel()
|
||||||
{
|
{
|
||||||
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
|
std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") };
|
||||||
if (!oss)
|
|
||||||
throw core::LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
|
|
||||||
|
|
||||||
boost::property_tree::xml_parser::write_xml(oss, pt);
|
if (tracingLevel == "disabled")
|
||||||
|
return std::nullopt;
|
||||||
|
else if (tracingLevel == "overview")
|
||||||
|
return core::tracing::Level::Overview;
|
||||||
|
else if (tracingLevel == "detailed")
|
||||||
|
return core::tracing::Level::Detailed;
|
||||||
|
|
||||||
if (!oss)
|
throw core::LmsException{ "Invalid config value for 'tracing-level'" };
|
||||||
throw core::LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
std::vector<std::string> generateWtConfig(std::string execPath, core::logging::Severity minSeverity)
|
||||||
}
|
{
|
||||||
|
std::vector<std::string> args;
|
||||||
|
|
||||||
void proxyScannerEventsToApplication(scanner::IScannerService& scanner, Wt::WServer& server)
|
const std::filesystem::path wtConfigPath{ core::Service<core::IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
|
||||||
{
|
const std::filesystem::path wtLogFilePath{ core::Service<core::IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
|
||||||
auto postAll{ [](Wt::WServer& server, std::function<void()> cb) {
|
const std::filesystem::path wtAccessLogFilePath{ core::Service<core::IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
|
||||||
server.postAll([cb = std::move(cb)] {
|
const std::filesystem::path wtResourcesPath{ core::Service<core::IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
|
||||||
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
|
||||||
if (LmsApp)
|
|
||||||
cb();
|
|
||||||
});
|
|
||||||
} };
|
|
||||||
|
|
||||||
scanner.getEvents().scanAborted.connect([&] {
|
args.push_back(execPath);
|
||||||
postAll(server, [] {
|
args.push_back("--config=" + wtConfigPath.string());
|
||||||
LmsApp->getScannerEvents().scanAborted.emit();
|
args.push_back("--docroot=" + std::string{ core::Service<core::IConfig>::get()->getString("docroot") });
|
||||||
LmsApp->triggerUpdate();
|
args.push_back("--approot=" + std::string{ core::Service<core::IConfig>::get()->getString("approot") });
|
||||||
});
|
args.push_back("--deploy-path=" + std::string{ core::Service<core::IConfig>::get()->getString("deploy-path", "/") });
|
||||||
});
|
if (!wtResourcesPath.empty())
|
||||||
|
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||||
|
|
||||||
scanner.getEvents().scanStarted.connect([&] {
|
if (core::Service<core::IConfig>::get()->getBool("tls-enable", false))
|
||||||
postAll(server, [] {
|
{
|
||||||
LmsApp->getScannerEvents().scanStarted.emit();
|
args.push_back("--https-port=" + std::to_string(core::Service<core::IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
LmsApp->triggerUpdate();
|
args.push_back("--https-address=" + std::string{ core::Service<core::IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
});
|
args.push_back("--ssl-certificate=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-cert") });
|
||||||
});
|
args.push_back("--ssl-private-key=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-key") });
|
||||||
|
args.push_back("--ssl-tmp-dh=" + std::string{ core::Service<core::IConfig>::get()->getString("tls-dh") });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
args.push_back("--http-port=" + std::to_string(core::Service<core::IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
|
args.push_back("--http-address=" + std::string{ core::Service<core::IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
|
}
|
||||||
|
|
||||||
scanner.getEvents().scanComplete.connect([&](const scanner::ScanStats& stats) {
|
if (!wtAccessLogFilePath.empty())
|
||||||
postAll(server, [=] {
|
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
||||||
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
|
||||||
LmsApp->triggerUpdate();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
scanner.getEvents().scanInProgress.connect([&](const scanner::ScanStepStats& stats) {
|
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
||||||
postAll(server, [=] {
|
|
||||||
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
|
||||||
LmsApp->triggerUpdate();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) {
|
// Generate the wt_config.xml file
|
||||||
postAll(server, [=] {
|
boost::property_tree::ptree pt;
|
||||||
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
|
||||||
LmsApp->triggerUpdate();
|
pt.put("server.application-settings.<xmlattr>.location", "*");
|
||||||
|
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||||
|
|
||||||
|
// log-config
|
||||||
|
pt.put("server.application-settings.log-config", core::logging::WtLogger::computeLogConfig(minSeverity));
|
||||||
|
|
||||||
|
// Reverse proxy
|
||||||
|
if (core::Service<core::IConfig>::get()->getBool("behind-reverse-proxy", false))
|
||||||
|
{
|
||||||
|
pt.put("server.application-settings.trusted-proxy-config.original-ip-header", core::Service<core::IConfig>::get()->getString("original-ip-header", "X-Forwarded-For"));
|
||||||
|
core::Service<core::IConfig>::get()->visitStrings("trusted-proxies", [&](std::string_view trustedProxy) {
|
||||||
|
pt.add("server.application-settings.trusted-proxy-config.trusted-proxies.proxy", std::string{ trustedProxy });
|
||||||
|
},
|
||||||
|
{ "127.0.0.1", "::1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree viewport;
|
||||||
|
viewport.put("<xmlattr>.name", "viewport");
|
||||||
|
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
boost::property_tree::ptree themeColor;
|
||||||
|
themeColor.put("<xmlattr>.name", "theme-color");
|
||||||
|
themeColor.put("<xmlattr>.content", "#303030");
|
||||||
|
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
|
||||||
|
if (!oss)
|
||||||
|
throw core::LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
|
||||||
|
|
||||||
|
boost::property_tree::xml_parser::write_xml(oss, pt);
|
||||||
|
|
||||||
|
if (!oss)
|
||||||
|
throw core::LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
void proxyScannerEventsToApplication(scanner::IScannerService& scanner, Wt::WServer& server)
|
||||||
|
{
|
||||||
|
auto postAll{ [](Wt::WServer& server, std::function<void()> cb) {
|
||||||
|
server.postAll([cb = std::move(cb)] {
|
||||||
|
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
||||||
|
if (LmsApp)
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
} };
|
||||||
|
|
||||||
|
scanner.getEvents().scanAborted.connect([&] {
|
||||||
|
postAll(server, [] {
|
||||||
|
LmsApp->getScannerEvents().scanAborted.emit();
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
scanner.getEvents().scanStarted.connect([&] {
|
||||||
|
postAll(server, [] {
|
||||||
|
LmsApp->getScannerEvents().scanStarted.emit();
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
scanner.getEvents().scanComplete.connect([&](const scanner::ScanStats& stats) {
|
||||||
|
postAll(server, [=] {
|
||||||
|
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
scanner.getEvents().scanInProgress.connect([&](const scanner::ScanStepStats& stats) {
|
||||||
|
postAll(server, [=] {
|
||||||
|
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) {
|
||||||
|
postAll(server, [=] {
|
||||||
|
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
||||||
|
LmsApp->triggerUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
std::filesystem::path configFilePath{ "/etc/lms.conf" };
|
std::filesystem::path configFilePath{ core::sysconfDirectory / "lms.conf" };
|
||||||
int res{ EXIT_FAILURE };
|
int res{ EXIT_FAILURE };
|
||||||
|
|
||||||
assert(argc > 0);
|
assert(argc > 0);
|
||||||
assert(argv[0] != NULL);
|
assert(argv[0] != NULL);
|
||||||
|
|
||||||
|
auto displayUsage{ [&](std::ostream& os) {
|
||||||
|
os << "Usage:\t" << argv[0] << "\t[conf_file]\n\n"
|
||||||
|
<< "Options:\n"
|
||||||
|
<< "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n";
|
||||||
|
} };
|
||||||
|
|
||||||
if (argc == 2)
|
if (argc == 2)
|
||||||
configFilePath = std::string(argv[1], 0, 256);
|
{
|
||||||
|
const std::string_view arg{ argv[1] };
|
||||||
|
if (arg == "-h" || arg == "--help")
|
||||||
|
{
|
||||||
|
displayUsage(std::cout);
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
configFilePath = std::string(arg, 0, 256);
|
||||||
|
}
|
||||||
else if (argc > 2)
|
else if (argc > 2)
|
||||||
{
|
{
|
||||||
std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n"
|
displayUsage(std::cerr);
|
||||||
<< "Options:\n"
|
|
||||||
<< "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n";
|
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +340,7 @@ namespace lms
|
|||||||
throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
|
throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
|
||||||
|
|
||||||
image::init(argv[0]);
|
image::init(argv[0]);
|
||||||
core::Service<cover::ICoverService> coverService{ cover::createCoverService(database, server.appRoot() + "/images/unknown-cover.svg") };
|
core::Service<cover::IArtworkService> artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") };
|
||||||
core::Service<recommendation::IRecommendationService> recommendationService{ recommendation::createRecommendationService(database) };
|
core::Service<recommendation::IRecommendationService> recommendationService{ recommendation::createRecommendationService(database) };
|
||||||
core::Service<recommendation::IPlaylistGeneratorService> playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
|
core::Service<recommendation::IPlaylistGeneratorService> playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
|
||||||
core::Service<scanner::IScannerService> scannerService{ scanner::createScannerService(database) };
|
core::Service<scanner::IScannerService> scannerService{ scanner::createScannerService(database) };
|
||||||
@@ -323,7 +348,7 @@ namespace lms
|
|||||||
scannerService->getEvents().scanComplete.connect([&] {
|
scannerService->getEvents().scanComplete.connect([&] {
|
||||||
// Flush cover cache even if no changes:
|
// Flush cover cache even if no changes:
|
||||||
// covers may be external files that changed and we don't keep track of them for now (but we should)
|
// covers may be external files that changed and we don't keep track of them for now (but we should)
|
||||||
coverService->flushCache();
|
artworkService->flushCache();
|
||||||
database.getTLSSession().refreshTracingLoggerStats();
|
database.getTLSSession().refreshTracingLoggerStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/TrackList.hpp"
|
#include "database/TrackList.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
#include "services/auth/IEnvService.hpp"
|
#include "services/auth/IEnvService.hpp"
|
||||||
#include "services/auth/IPasswordService.hpp"
|
#include "services/auth/IPasswordService.hpp"
|
||||||
#include "services/cover/ICoverService.hpp"
|
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
|
||||||
#include "Auth.hpp"
|
#include "Auth.hpp"
|
||||||
@@ -61,9 +61,9 @@
|
|||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "explore/Explore.hpp"
|
#include "explore/Explore.hpp"
|
||||||
#include "explore/Filters.hpp"
|
#include "explore/Filters.hpp"
|
||||||
|
#include "resource/ArtworkResource.hpp"
|
||||||
#include "resource/AudioFileResource.hpp"
|
#include "resource/AudioFileResource.hpp"
|
||||||
#include "resource/AudioTranscodingResource.hpp"
|
#include "resource/AudioTranscodingResource.hpp"
|
||||||
#include "resource/CoverResource.hpp"
|
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
@@ -400,7 +400,7 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationCreateHome");
|
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationCreateHome");
|
||||||
|
|
||||||
_coverResource = std::make_shared<CoverResource>();
|
_artworkResource = std::make_shared<ArtworkResource>();
|
||||||
|
|
||||||
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}");
|
||||||
declareJavaScriptFunction("updateActiveNav",
|
declareJavaScriptFunction("updateActiveNav",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
class CoverResource;
|
class ArtworkResource;
|
||||||
class LmsApplicationException;
|
class LmsApplicationException;
|
||||||
class MediaPlayer;
|
class MediaPlayer;
|
||||||
class PlayQueue;
|
class PlayQueue;
|
||||||
@@ -60,7 +60,7 @@ namespace lms::ui
|
|||||||
static LmsApplication* instance();
|
static LmsApplication* instance();
|
||||||
|
|
||||||
// Session application data
|
// Session application data
|
||||||
std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; }
|
std::shared_ptr<ArtworkResource> getArtworkResource() { return _artworkResource; }
|
||||||
db::Db& getDb();
|
db::Db& getDb();
|
||||||
db::Session& getDbSession(); // always thread safe
|
db::Session& getDbSession(); // always thread safe
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ namespace lms::ui
|
|||||||
bool strongAuth{};
|
bool strongAuth{};
|
||||||
};
|
};
|
||||||
std::optional<UserAuthInfo> _user;
|
std::optional<UserAuthInfo> _user;
|
||||||
std::shared_ptr<CoverResource> _coverResource;
|
std::shared_ptr<ArtworkResource> _artworkResource;
|
||||||
MediaPlayer* _mediaPlayer{};
|
MediaPlayer* _mediaPlayer{};
|
||||||
PlayQueue* _playQueue{};
|
PlayQueue* _playQueue{};
|
||||||
NotificationContainer* _notificationContainer{};
|
NotificationContainer* _notificationContainer{};
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "resource/ArtworkResource.hpp"
|
||||||
#include "resource/AudioFileResource.hpp"
|
#include "resource/AudioFileResource.hpp"
|
||||||
#include "resource/AudioTranscodingResource.hpp"
|
#include "resource/AudioTranscodingResource.hpp"
|
||||||
#include "resource/CoverResource.hpp"
|
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
@@ -255,13 +255,13 @@ namespace lms::ui
|
|||||||
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
||||||
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
||||||
<< " artwork: ["
|
<< " artwork: ["
|
||||||
<< " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
<< " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
||||||
<< " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
<< " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
||||||
<< " ]"
|
<< " ]"
|
||||||
<< "};";
|
<< "};";
|
||||||
// Update 'sizes' above to match this:
|
// Update 'sizes' above to match this:
|
||||||
static_assert(static_cast<std::underlying_type_t<CoverResource::Size>>(CoverResource::Size::Small) == 128);
|
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Small) == 128);
|
||||||
static_assert(static_cast<std::underlying_type_t<CoverResource::Size>>(CoverResource::Size::Large) == 512);
|
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Large) == 512);
|
||||||
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
oss << "LMS.mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||||
|
|
||||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||||
|
|||||||
@@ -485,22 +485,20 @@ namespace lms::ui
|
|||||||
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists));
|
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto release{ track->getRelease() };
|
auto image{ utils::createTrackImage(track->getId(), ArtworkResource::Size::Small) };
|
||||||
if (release)
|
image->addStyleClass("Lms-cover-track");
|
||||||
|
if (const auto release{ track->getRelease() })
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-release", true);
|
entry->setCondition("if-has-release", true);
|
||||||
entry->bindWidget("release", utils::createReleaseAnchor(release));
|
entry->bindWidget("release", utils::createReleaseAnchor(release));
|
||||||
{
|
|
||||||
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
||||||
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Small) };
|
image->addStyleClass("Lms-cover-anchor"); // HACK
|
||||||
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
|
anchor->setImage(std::move(image));
|
||||||
anchor->setImage(std::move(cover));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auto cover{ entry->bindWidget<Wt::WImage>("cover", utils::createCover(track->getId(), CoverResource::Size::Small)) };
|
entry->bindWidget("cover", std::move(image));
|
||||||
cover->addStyleClass("Lms-cover-track");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|||||||
+50
-18
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
#include "MediaPlayer.hpp"
|
#include "MediaPlayer.hpp"
|
||||||
|
#include "Tooltip.hpp"
|
||||||
#include "common/DoubleValidator.hpp"
|
#include "common/DoubleValidator.hpp"
|
||||||
#include "common/MandatoryValidator.hpp"
|
#include "common/MandatoryValidator.hpp"
|
||||||
#include "common/PasswordValidator.hpp"
|
#include "common/PasswordValidator.hpp"
|
||||||
@@ -51,6 +52,7 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Associate each field with a unique string literal.
|
// Associate each field with a unique string literal.
|
||||||
|
static inline const Field ArtistReleaseSortMethodField{ "artist-release-sort-method" };
|
||||||
static inline const Field TranscodingModeField{ "transcoding-mode" };
|
static inline const Field TranscodingModeField{ "transcoding-mode" };
|
||||||
static inline const Field TranscodeFormatField{ "transcoding-output-format" };
|
static inline const Field TranscodeFormatField{ "transcoding-output-format" };
|
||||||
static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" };
|
static inline const Field TranscodeBitrateField{ "transcoding-output-bitrate" };
|
||||||
@@ -68,10 +70,11 @@ namespace lms::ui
|
|||||||
static inline const Field PasswordField{ "password" };
|
static inline const Field PasswordField{ "password" };
|
||||||
static inline const Field PasswordConfirmField{ "password-confirm" };
|
static inline const Field PasswordConfirmField{ "password-confirm" };
|
||||||
|
|
||||||
|
using ArtistReleaseSortMethodModel = ValueStringModel<db::ReleaseSortMethod>;
|
||||||
using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcoding::Mode>;
|
using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcoding::Mode>;
|
||||||
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
|
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
|
||||||
using FeedbackBackendModel = ValueStringModel<FeedbackBackend>;
|
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
|
||||||
using ScrobblingBackendModel = ValueStringModel<ScrobblingBackend>;
|
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
|
||||||
|
|
||||||
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword)
|
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword)
|
||||||
: _authPasswordService{ authPasswordService }
|
: _authPasswordService{ authPasswordService }
|
||||||
@@ -79,6 +82,7 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
initializeModels();
|
initializeModels();
|
||||||
|
|
||||||
|
addField(ArtistReleaseSortMethodField);
|
||||||
addField(TranscodingModeField);
|
addField(TranscodingModeField);
|
||||||
addField(TranscodeBitrateField);
|
addField(TranscodeBitrateField);
|
||||||
addField(TranscodeFormatField);
|
addField(TranscodeFormatField);
|
||||||
@@ -106,6 +110,7 @@ namespace lms::ui
|
|||||||
addField(PasswordConfirmField);
|
addField(PasswordConfirmField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setValidator(ArtistReleaseSortMethodField, createMandatoryValidator());
|
||||||
setValidator(TranscodingModeField, createMandatoryValidator());
|
setValidator(TranscodingModeField, createMandatoryValidator());
|
||||||
setValidator(TranscodeBitrateField, createMandatoryValidator());
|
setValidator(TranscodeBitrateField, createMandatoryValidator());
|
||||||
setValidator(TranscodeFormatField, createMandatoryValidator());
|
setValidator(TranscodeFormatField, createMandatoryValidator());
|
||||||
@@ -122,6 +127,7 @@ namespace lms::ui
|
|||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ArtistReleaseSortMethodModel> getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
|
||||||
std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
|
std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
|
||||||
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
|
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
|
||||||
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
|
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
|
||||||
@@ -136,6 +142,12 @@ namespace lms::ui
|
|||||||
|
|
||||||
User::pointer user{ LmsApp->getUser() };
|
User::pointer user{ LmsApp->getUser() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) };
|
||||||
|
if (artistReleaseSortMethodRow)
|
||||||
|
user.modify()->setUIArtistReleaseSortMethod(_artistReleaseSortMethodModel->getValue(*artistReleaseSortMethodRow));
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
MediaPlayer::Settings settings;
|
MediaPlayer::Settings settings;
|
||||||
|
|
||||||
@@ -204,6 +216,12 @@ namespace lms::ui
|
|||||||
|
|
||||||
User::pointer user{ LmsApp->getUser() };
|
User::pointer user{ LmsApp->getUser() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) };
|
||||||
|
if (artistReleaseSortMethodRow)
|
||||||
|
setValue(ArtistReleaseSortMethodField, _artistReleaseSortMethodModel->getString(*artistReleaseSortMethodRow));
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto settings{ *LmsApp->getMediaPlayer().getSettings() };
|
const auto settings{ *LmsApp->getMediaPlayer().getSettings() };
|
||||||
|
|
||||||
@@ -315,6 +333,13 @@ namespace lms::ui
|
|||||||
private:
|
private:
|
||||||
void initializeModels()
|
void initializeModels()
|
||||||
{
|
{
|
||||||
|
_artistReleaseSortMethodModel = std::make_shared<ArtistReleaseSortMethodModel>();
|
||||||
|
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-asc"), db::ReleaseSortMethod::DateAsc);
|
||||||
|
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.date-desc"), db::ReleaseSortMethod::DateDesc);
|
||||||
|
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-asc"), db::ReleaseSortMethod::OriginalDate);
|
||||||
|
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.original-date-desc"), db::ReleaseSortMethod::OriginalDateDesc);
|
||||||
|
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name);
|
||||||
|
|
||||||
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
|
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
|
||||||
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
|
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
|
||||||
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never);
|
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.never"), MediaPlayer::Settings::Transcoding::Mode::Never);
|
||||||
@@ -355,6 +380,7 @@ namespace lms::ui
|
|||||||
auth::IPasswordService* _authPasswordService{};
|
auth::IPasswordService* _authPasswordService{};
|
||||||
bool _withOldPassword{};
|
bool _withOldPassword{};
|
||||||
|
|
||||||
|
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
|
||||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
||||||
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
|
||||||
@@ -425,12 +451,25 @@ namespace lms::ui
|
|||||||
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
|
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User interface
|
||||||
|
{
|
||||||
|
auto artistReleaseSortMethod{ std::make_unique<Wt::WComboBox>() };
|
||||||
|
artistReleaseSortMethod->setModel(model->getArtistReleaseSortMethodModel());
|
||||||
|
t->setFormWidget(SettingsModel::ArtistReleaseSortMethodField, std::move(artistReleaseSortMethod));
|
||||||
|
}
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
{
|
{
|
||||||
// Transcode
|
// Transcode
|
||||||
auto transcodingMode{ std::make_unique<Wt::WComboBox>() };
|
auto transcodingMode{ std::make_unique<Wt::WComboBox>() };
|
||||||
auto* transcodingModeRaw{ transcodingMode.get() };
|
|
||||||
transcodingMode->setModel(model->getTranscodingModeModel());
|
transcodingMode->setModel(model->getTranscodingModeModel());
|
||||||
|
transcodingMode->activated().connect([=](int row) {
|
||||||
|
const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcoding::Mode::Never };
|
||||||
|
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
|
||||||
|
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
|
||||||
|
t->updateModel(model.get());
|
||||||
|
t->updateView(model.get());
|
||||||
|
});
|
||||||
t->setFormWidget(SettingsModel::TranscodingModeField, std::move(transcodingMode));
|
t->setFormWidget(SettingsModel::TranscodingModeField, std::move(transcodingMode));
|
||||||
|
|
||||||
// Format
|
// Format
|
||||||
@@ -443,17 +482,15 @@ namespace lms::ui
|
|||||||
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
|
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
|
||||||
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
|
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
|
||||||
|
|
||||||
transcodingModeRaw->activated().connect([=](int row) {
|
// Replay gain mode
|
||||||
const bool enable{ model->getTranscodingModeModel()->getValue(row) != MediaPlayer::Settings::Transcoding::Mode::Never };
|
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
|
||||||
model->setReadOnly(SettingsModel::TranscodeFormatField, !enable);
|
replayGainMode->activated().connect([=](int row) {
|
||||||
model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable);
|
const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None };
|
||||||
|
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable);
|
||||||
|
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable);
|
||||||
t->updateModel(model.get());
|
t->updateModel(model.get());
|
||||||
t->updateView(model.get());
|
t->updateView(model.get());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replay gain mode
|
|
||||||
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
|
|
||||||
auto* replayGainModeRaw{ replayGainMode.get() };
|
|
||||||
replayGainMode->setModel(model->getReplayGainModeModel());
|
replayGainMode->setModel(model->getReplayGainModeModel());
|
||||||
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
|
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
|
||||||
|
|
||||||
@@ -467,13 +504,6 @@ namespace lms::ui
|
|||||||
replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||||
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo));
|
t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo));
|
||||||
|
|
||||||
replayGainModeRaw->activated().connect([=](int row) {
|
|
||||||
const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None };
|
|
||||||
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable);
|
|
||||||
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable);
|
|
||||||
t->updateModel(model.get());
|
|
||||||
t->updateView(model.get());
|
|
||||||
});
|
|
||||||
if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None)
|
if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None)
|
||||||
{
|
{
|
||||||
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
|
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
|
||||||
@@ -573,6 +603,8 @@ namespace lms::ui
|
|||||||
});
|
});
|
||||||
|
|
||||||
t->updateView(model.get());
|
t->updateView(model.get());
|
||||||
|
|
||||||
|
initTooltipsForWidgetTree(*t);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Tooltip.hpp"
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include <Wt/WWebWidget.h>
|
||||||
|
|
||||||
|
namespace lms::ui
|
||||||
|
{
|
||||||
|
void initTooltipsForWidgetTree(Wt::WWebWidget& widget)
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << R"({const rootElement = document.getElementById(')" << widget.id() << R"(');)"
|
||||||
|
<< R"(const tooltipTriggerList = rootElement.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
tooltipTriggerList.forEach(tooltipTriggerEl => {
|
||||||
|
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});})";
|
||||||
|
LMS_LOG(UI, DEBUG, "Running JS '" << oss.str() << "'");
|
||||||
|
|
||||||
|
widget.doJavaScript(oss.str());
|
||||||
|
}
|
||||||
|
} // namespace lms::ui
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Wt
|
||||||
|
{
|
||||||
|
class WWebWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace lms::ui
|
||||||
|
{
|
||||||
|
void initTooltipsForWidgetTree(Wt::WWebWidget& widget);
|
||||||
|
}
|
||||||
+23
-6
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
@@ -62,21 +63,31 @@ namespace lms::ui::utils
|
|||||||
return oss.str();
|
return oss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createCover(db::ReleaseId releaseId, CoverResource::Size size)
|
std::unique_ptr<Wt::WImage> createArtistImage(db::ArtistId artistId, ArtworkResource::Size size)
|
||||||
|
{
|
||||||
|
auto image{ std::make_unique<Wt::WImage>() };
|
||||||
|
image->setImageLink(LmsApp->getArtworkResource()->getArtistImageUrl(artistId, size));
|
||||||
|
image->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||||
|
image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WImage> createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size)
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto cover{ std::make_unique<Wt::WImage>() };
|
||||||
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
|
cover->setImageLink(LmsApp->getArtworkResource()->getReleaseCoverUrl(releaseId, size));
|
||||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||||
return cover;
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createCover(db::TrackId trackId, CoverResource::Size size)
|
std::unique_ptr<Wt::WImage> createTrackImage(db::TrackId trackId, ArtworkResource::Size size)
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto cover{ std::make_unique<Wt::WImage>() };
|
||||||
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
|
cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size));
|
||||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||||
|
|
||||||
return cover;
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +223,14 @@ namespace lms::ui::utils
|
|||||||
matchCount += 1;
|
matchCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchCount != artistIds.size())
|
if (matchCount == artistIds.size())
|
||||||
return createArtistAnchorList(artistIds, cssAnchorClass);
|
{
|
||||||
|
const std::string_view remainingStr{ displayName.substr(currentOffset) };
|
||||||
|
if (!remainingStr.empty())
|
||||||
|
result->addNew<Wt::WText>(std::string{ remainingStr }, Wt::TextFormat::Plain);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
result = createArtistAnchorList(artistIds, cssAnchorClass);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
|
|
||||||
#include "resource/CoverResource.hpp"
|
#include "resource/ArtworkResource.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
@@ -54,8 +54,9 @@ namespace lms::ui::utils
|
|||||||
{
|
{
|
||||||
std::string durationToString(std::chrono::milliseconds msDuration);
|
std::string durationToString(std::chrono::milliseconds msDuration);
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createCover(db::ReleaseId releaseId, CoverResource::Size size);
|
std::unique_ptr<Wt::WImage> createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size);
|
||||||
std::unique_ptr<Wt::WImage> createCover(db::TrackId trackId, CoverResource::Size size);
|
std::unique_ptr<Wt::WImage> createTrackImage(db::TrackId trackId, ArtworkResource::Size size);
|
||||||
|
std::unique_ptr<Wt::WImage> createArtistImage(db::ArtistId artistId, ArtworkResource::Size size);
|
||||||
|
|
||||||
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
|
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
|
||||||
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
|
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
|
||||||
|
|||||||
@@ -274,6 +274,11 @@ namespace lms::ui
|
|||||||
.arg(stepStats.progress()));
|
.arg(stepStats.progress()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ScanStep::AssociateReleaseImages:
|
||||||
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-release-images")
|
||||||
|
.arg(stepStats.progress()));
|
||||||
|
break;
|
||||||
|
|
||||||
case ScanStep::CheckForDuplicatedFiles:
|
case ScanStep::CheckForDuplicatedFiles:
|
||||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
|
||||||
.arg(stepStats.processedElems));
|
.arg(stepStats.processedElems));
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace lms::ui
|
|||||||
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
return Wt::cpp17::any_cast<Wt::WString>(data(index(static_cast<int>(row), 0), Wt::ItemDataRole::Display));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::size_t> getRowFromString(const Wt::WString& value)
|
std::optional<std::size_t> getRowFromString(const Wt::WString& value) const
|
||||||
{
|
{
|
||||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||||
{
|
{
|
||||||
@@ -51,7 +51,7 @@ namespace lms::ui
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::size_t> getRowFromValue(const T& value)
|
std::optional<std::size_t> getRowFromValue(const T& value) const
|
||||||
{
|
{
|
||||||
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
for (std::size_t i{}; i < static_cast<std::size_t>(rowCount()); ++i)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,9 +28,14 @@ namespace lms::ui::ArtistListHelpers
|
|||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Artist>& artist)
|
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Artist>& artist)
|
||||||
{
|
{
|
||||||
auto res{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
|
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
|
||||||
res->bindWidget("name", utils::createArtistAnchor(artist));
|
entry->bindWidget("name", utils::createArtistAnchor(artist));
|
||||||
|
|
||||||
return res;
|
Wt::WAnchor* anchor{ entry->bindWidget("image", utils::createArtistAnchor(artist, false)) };
|
||||||
|
auto image{ utils::createArtistImage(artist->getId(), ArtworkResource::Size::Large) };
|
||||||
|
image->addStyleClass("Lms-cover-release Lms-cover-anchor");
|
||||||
|
anchor->setImage(std::move(image));
|
||||||
|
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
} // namespace lms::ui::ArtistListHelpers
|
} // namespace lms::ui::ArtistListHelpers
|
||||||
@@ -108,7 +108,7 @@ namespace lms::ui
|
|||||||
if (!artistId)
|
if (!artistId)
|
||||||
throw ArtistNotFoundException{};
|
throw ArtistNotFoundException{};
|
||||||
|
|
||||||
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, 5) };
|
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, 6) };
|
||||||
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ namespace lms::ui
|
|||||||
params.setClusters(_filters.getClusters());
|
params.setClusters(_filters.getClusters());
|
||||||
params.setMediaLibrary(_filters.getMediaLibrary());
|
params.setMediaLibrary(_filters.getMediaLibrary());
|
||||||
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
|
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
|
||||||
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
|
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
|
||||||
|
|
||||||
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
|
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
|
||||||
if (!releases.results.empty())
|
if (!releases.results.empty())
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ namespace lms::ui::releaseListHelpers
|
|||||||
|
|
||||||
{
|
{
|
||||||
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
||||||
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Large) };
|
auto image{ utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large) };
|
||||||
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
|
image->addStyleClass("Lms-cover-release Lms-cover-anchor"); // hack
|
||||||
anchor->setImage(std::move(cover));
|
anchor->setImage(std::move(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
|
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
refreshReleaseArtists(release);
|
refreshReleaseArtists(release);
|
||||||
|
|
||||||
bindWidget<Wt::WImage>("cover", utils::createCover(release->getId(), CoverResource::Size::Large));
|
bindWidget<Wt::WImage>("cover", utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large));
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
|
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
#include "explore/PlayQueueController.hpp"
|
#include "explore/PlayQueueController.hpp"
|
||||||
#include "resource/CoverResource.hpp"
|
#include "resource/ArtworkResource.hpp"
|
||||||
#include "resource/DownloadResource.hpp"
|
#include "resource/DownloadResource.hpp"
|
||||||
|
|
||||||
namespace lms::ui::TrackListHelpers
|
namespace lms::ui::TrackListHelpers
|
||||||
@@ -173,20 +173,20 @@ namespace lms::ui::TrackListHelpers
|
|||||||
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto image{ utils::createTrackImage(trackId, ArtworkResource::Size::Small) };
|
||||||
|
image->addStyleClass("Lms-cover-track");
|
||||||
if (track->getRelease())
|
if (track->getRelease())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-release", true);
|
entry->setCondition("if-has-release", true);
|
||||||
entry->bindWidget("release", utils::createReleaseAnchor(track->getRelease()));
|
entry->bindWidget("release", utils::createReleaseAnchor(track->getRelease()));
|
||||||
|
|
||||||
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
||||||
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Small) };
|
image->addStyleClass("Lms-cover-anchor"); // HACK
|
||||||
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
|
anchor->setImage(std::move((image)));
|
||||||
anchor->setImage(std::move((cover)));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auto cover{ utils::createCover(trackId, CoverResource::Size::Small) };
|
entry->bindWidget<Wt::WImage>("cover", std::move(image));
|
||||||
cover->addStyleClass("Lms-cover-track"); // HACK
|
|
||||||
entry->bindWidget<Wt::WImage>("cover", std::move(cover));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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 "ArtworkResource.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Http/Response.h>
|
||||||
|
#include <Wt/WApplication.h>
|
||||||
|
|
||||||
|
#include "core/Exception.hpp"
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/ITraceLogger.hpp"
|
||||||
|
#include "core/Service.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
|
|
||||||
|
#define LOG(severity, message) LMS_LOG(UI, severity, "Image resource: " << message)
|
||||||
|
|
||||||
|
namespace lms::ui
|
||||||
|
{
|
||||||
|
ArtworkResource::ArtworkResource()
|
||||||
|
{
|
||||||
|
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const scanner::ScanStats& stats) {
|
||||||
|
if (stats.nbChanges())
|
||||||
|
setChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtworkResource::~ArtworkResource()
|
||||||
|
{
|
||||||
|
beingDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getArtistImageUrl(db::ArtistId artistId, Size size) const
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Artist::pointer artist{ db::Artist::find(LmsApp->getDbSession(), artistId) };
|
||||||
|
if (artist && artist->getImage())
|
||||||
|
url = getArtistIdImageUrl(artistId, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.empty())
|
||||||
|
url = getDefaultArtistImageUrl();
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getReleaseCoverUrl(db::ReleaseId releaseId, Size size) const
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) };
|
||||||
|
if (release)
|
||||||
|
{
|
||||||
|
if (release->getImage())
|
||||||
|
{
|
||||||
|
url = getReleaseIdCoverUrl(release->getId(), size);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
db::Track::FindParameters params;
|
||||||
|
params.setRelease(releaseId);
|
||||||
|
params.setHasEmbeddedImage(true);
|
||||||
|
params.setRange(db::Range{ 0, 1 });
|
||||||
|
|
||||||
|
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
|
||||||
|
url = getTrackIdImageUrl(track->getId(), size);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.empty())
|
||||||
|
url = getDefaultReleaseCoverUrl();
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getTrackImageUrl(db::TrackId trackId, Size size) const
|
||||||
|
{
|
||||||
|
std::string url;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||||
|
|
||||||
|
const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) };
|
||||||
|
if (track)
|
||||||
|
{
|
||||||
|
if (track->hasCover())
|
||||||
|
url = getTrackIdImageUrl(trackId, size);
|
||||||
|
else if (const db::Release::pointer release{ track->getRelease() }; release && release->getImage())
|
||||||
|
url = getReleaseIdCoverUrl(release->getId(), size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.empty())
|
||||||
|
url = getDefaultReleaseCoverUrl();
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getArtistIdImageUrl(db::ArtistId artistId, Size size) const
|
||||||
|
{
|
||||||
|
return url() + "&artistid=" + artistId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getReleaseIdCoverUrl(db::ReleaseId releaseId, Size size) const
|
||||||
|
{
|
||||||
|
return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getTrackIdImageUrl(db::TrackId trackId, Size size) const
|
||||||
|
{
|
||||||
|
return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getDefaultArtistImageUrl() const
|
||||||
|
{
|
||||||
|
return url() + "&type=defaultartistimage";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ArtworkResource::getDefaultReleaseCoverUrl() const
|
||||||
|
{
|
||||||
|
return url() + "&type=defaultreleasecover";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtworkResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
LMS_SCOPED_TRACE_OVERVIEW("UI", "HandleCoverRequest");
|
||||||
|
|
||||||
|
// Retrieve parameters
|
||||||
|
const std::string* artistIdStr = request.getParameter("artistid");
|
||||||
|
const std::string* trackIdStr = request.getParameter("trackid");
|
||||||
|
const std::string* releaseIdStr = request.getParameter("releaseid");
|
||||||
|
const std::string* sizeStr = request.getParameter("size");
|
||||||
|
const std::string* typeStr = request.getParameter("type");
|
||||||
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
|
// Mandatory parameter size
|
||||||
|
if ((artistIdStr || trackIdStr || releaseIdStr))
|
||||||
|
{
|
||||||
|
if (!sizeStr)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "no size provided!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto size{ core::stringUtils::readAs<std::size_t>(*sizeStr) };
|
||||||
|
if (!size || *size > maxSize)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "invalid size provided!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistIdStr)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "Requested cover for track " << *artistIdStr << ", size = " << *size);
|
||||||
|
|
||||||
|
const std::optional<db::ArtistId> artistId{ core::stringUtils::readAs<db::ArtistId::ValueType>(*artistIdStr) };
|
||||||
|
if (!artistId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getArtistImage(*artistId, *size);
|
||||||
|
if (!image)
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getDefaultArtistImage();
|
||||||
|
}
|
||||||
|
else if (releaseIdStr)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "Requested cover for release " << *releaseIdStr << ", size = " << *size);
|
||||||
|
|
||||||
|
const std::optional<db::ReleaseId> releaseId{ core::stringUtils::readAs<db::ReleaseId::ValueType>(*releaseIdStr) };
|
||||||
|
if (!releaseId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getReleaseCover(*releaseId, *size);
|
||||||
|
if (!image)
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
|
||||||
|
}
|
||||||
|
else if (trackIdStr)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "Requested cover for track " << *trackIdStr << ", size = " << *size);
|
||||||
|
|
||||||
|
const std::optional<db::TrackId> trackId{ core::stringUtils::readAs<db::TrackId::ValueType>(*trackIdStr) };
|
||||||
|
if (!trackId)
|
||||||
|
{
|
||||||
|
LOG(DEBUG, "track not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getTrackImage(*trackId, *size);
|
||||||
|
if (!image)
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (typeStr)
|
||||||
|
{
|
||||||
|
if (*typeStr == "defaultreleasecover")
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
|
||||||
|
else if (*typeStr == "defaultartistimage")
|
||||||
|
image = core::Service<cover::IArtworkService>::get()->getDefaultArtistImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image)
|
||||||
|
{
|
||||||
|
response.setMimeType(std::string{ image->getMimeType() });
|
||||||
|
response.out().write(reinterpret_cast<const char*>(image->getData()), image->getDataSize());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
response.setStatus(404);
|
||||||
|
}
|
||||||
|
} // namespace lms::ui
|
||||||
@@ -21,18 +21,19 @@
|
|||||||
|
|
||||||
#include <Wt/WResource.h>
|
#include <Wt/WResource.h>
|
||||||
|
|
||||||
|
#include "database/ArtistId.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
class CoverResource : public Wt::WResource
|
class ArtworkResource : public Wt::WResource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static const std::size_t maxSize{ 512 };
|
static const std::size_t maxSize{ 512 };
|
||||||
|
|
||||||
CoverResource();
|
ArtworkResource();
|
||||||
~CoverResource();
|
~ArtworkResource();
|
||||||
|
|
||||||
enum class Size : std::size_t
|
enum class Size : std::size_t
|
||||||
{
|
{
|
||||||
@@ -40,10 +41,18 @@ namespace lms::ui
|
|||||||
Large = 512,
|
Large = 512,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string getReleaseUrl(db::ReleaseId releaseId, Size size) const;
|
std::string getArtistImageUrl(db::ArtistId artistId, Size size) const;
|
||||||
std::string getTrackUrl(db::TrackId trackId, Size size) const;
|
std::string getReleaseCoverUrl(db::ReleaseId releaseId, Size size) const;
|
||||||
|
std::string getTrackImageUrl(db::TrackId trackId, Size size) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::string getArtistIdImageUrl(db::ArtistId artistId, Size size) const;
|
||||||
|
std::string getReleaseIdCoverUrl(db::ReleaseId releaseId, Size size) const;
|
||||||
|
std::string getTrackIdImageUrl(db::TrackId trackId, Size size) const;
|
||||||
|
|
||||||
|
std::string getDefaultArtistImageUrl() const;
|
||||||
|
std::string getDefaultReleaseCoverUrl() const;
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2014 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 "CoverResource.hpp"
|
|
||||||
|
|
||||||
#include <Wt/Http/Response.h>
|
|
||||||
#include <Wt/WApplication.h>
|
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
|
||||||
#include "core/ILogger.hpp"
|
|
||||||
#include "core/ITraceLogger.hpp"
|
|
||||||
#include "core/Service.hpp"
|
|
||||||
#include "core/String.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "services/cover/ICoverService.hpp"
|
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
|
|
||||||
#define LOG(severity, message) LMS_LOG(UI, severity, "Image resource: " << message)
|
|
||||||
|
|
||||||
namespace lms::ui
|
|
||||||
{
|
|
||||||
CoverResource::CoverResource()
|
|
||||||
{
|
|
||||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const scanner::ScanStats& stats) {
|
|
||||||
if (stats.nbChanges())
|
|
||||||
setChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
CoverResource::~CoverResource()
|
|
||||||
{
|
|
||||||
beingDeleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string CoverResource::getReleaseUrl(db::ReleaseId releaseId, Size size) const
|
|
||||||
{
|
|
||||||
return url() + "&releaseid=" + releaseId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string CoverResource::getTrackUrl(db::TrackId trackId, Size size) const
|
|
||||||
{
|
|
||||||
return url() + "&trackid=" + trackId.toString() + "&size=" + std::to_string(static_cast<std::size_t>(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
void CoverResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
|
||||||
{
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("UI", "HandleCoverRequest");
|
|
||||||
|
|
||||||
// Retrieve parameters
|
|
||||||
const std::string* trackIdStr = request.getParameter("trackid");
|
|
||||||
const std::string* releaseIdStr = request.getParameter("releaseid");
|
|
||||||
const std::string* sizeStr = request.getParameter("size");
|
|
||||||
|
|
||||||
// Mandatory parameter size
|
|
||||||
if (!sizeStr)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "no size provided!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto size{ core::stringUtils::readAs<std::size_t>(*sizeStr) };
|
|
||||||
if (!size || *size > maxSize)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "invalid size provided!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> cover;
|
|
||||||
|
|
||||||
if (trackIdStr)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "Requested cover for track " << *trackIdStr << ", size = " << *size);
|
|
||||||
|
|
||||||
const std::optional<db::TrackId> trackId{ core::stringUtils::readAs<db::TrackId::ValueType>(*trackIdStr) };
|
|
||||||
if (!trackId)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "track not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cover = core::Service<cover::ICoverService>::get()->getFromTrack(*trackId, *size);
|
|
||||||
if (!cover)
|
|
||||||
cover = core::Service<cover::ICoverService>::get()->getDefaultSvgCover();
|
|
||||||
}
|
|
||||||
else if (releaseIdStr)
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "Requested cover for release " << *releaseIdStr << ", size = " << *size);
|
|
||||||
|
|
||||||
const std::optional<db::ReleaseId> releaseId{ core::stringUtils::readAs<db::ReleaseId::ValueType>(*releaseIdStr) };
|
|
||||||
if (!releaseId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
cover = core::Service<cover::ICoverService>::get()->getFromRelease(*releaseId, *size);
|
|
||||||
if (!cover)
|
|
||||||
cover = core::Service<cover::ICoverService>::get()->getDefaultSvgCover();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG(DEBUG, "No track or release provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setMimeType(std::string{ cover->getMimeType() });
|
|
||||||
|
|
||||||
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
|
||||||
}
|
|
||||||
} // namespace lms::ui
|
|
||||||
@@ -4,7 +4,7 @@ add_executable(lms-cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(lms-cover PRIVATE
|
target_link_libraries(lms-cover PRIVATE
|
||||||
lmsservice-cover
|
lmsartwork
|
||||||
Boost::program_options
|
Boost::program_options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,13 @@
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/StreamLogger.hpp"
|
#include "core/StreamLogger.hpp"
|
||||||
|
#include "core/SystemPaths.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
#include "services/cover/ICoverService.hpp"
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
namespace lms
|
namespace lms
|
||||||
{
|
{
|
||||||
@@ -50,7 +51,7 @@ namespace lms
|
|||||||
for (const db::TrackId trackId : trackIds.results)
|
for (const db::TrackId trackId : trackIds.results)
|
||||||
{
|
{
|
||||||
std::cout << "Getting cover for track id " << trackId.toString() << std::endl;
|
std::cout << "Getting cover for track id " << trackId.toString() << std::endl;
|
||||||
core::Service<cover::ICoverService>::get()->getFromTrack(trackId, width);
|
core::Service<cover::IArtworkService>::get()->getTrackImage(trackId, width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms
|
} // namespace lms
|
||||||
@@ -66,7 +67,17 @@ int main(int argc, char* argv[])
|
|||||||
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
|
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
|
||||||
|
|
||||||
po::options_description desc{ "Allowed options" };
|
po::options_description desc{ "Allowed options" };
|
||||||
desc.add_options()("help,h", "print usage message")("conf,c", po::value<std::string>()->default_value("/etc/lms.conf"), "LMS config file")("default-cover,d", po::value<std::string>(), "Default cover path")("tracks,t", "dump covers for tracks")("size,s", po::value<unsigned>()->default_value(512), "Requested cover size")("quality,q", po::value<unsigned>()->default_value(75), "JPEG quality (1-100)");
|
|
||||||
|
// clang-format off
|
||||||
|
desc.add_options()
|
||||||
|
("help,h", "print usage message")
|
||||||
|
("conf,c", po::value<std::string>()->default_value(core::sysconfDirectory / "lms.conf"), "LMS config file")
|
||||||
|
("default-release-cover,d", po::value<std::string>(), "Default release cover path")
|
||||||
|
("default-artist-image,d", po::value<std::string>(), "Default artist image")
|
||||||
|
("tracks,t", "dump covers for tracks")
|
||||||
|
("size,s", po::value<unsigned>()->default_value(512), "Requested cover size")
|
||||||
|
("quality,q", po::value<unsigned>()->default_value(75), "JPEG quality (1-100)");
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
po::variables_map vm;
|
po::variables_map vm;
|
||||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||||
@@ -80,7 +91,7 @@ int main(int argc, char* argv[])
|
|||||||
image::init(argv[0]);
|
image::init(argv[0]);
|
||||||
core::Service<core::IConfig> config{ core::createConfig(vm["conf"].as<std::string>()) };
|
core::Service<core::IConfig> config{ core::createConfig(vm["conf"].as<std::string>()) };
|
||||||
db::Db db{ config->getPath("working-dir") / "lms.db" };
|
db::Db db{ config->getPath("working-dir") / "lms.db" };
|
||||||
core::Service<cover::ICoverService> coverArtService{ cover::createCoverService(db, vm["default-cover"].as<std::string>()) };
|
core::Service<cover::IArtworkService> coverArtService{ cover::createArtworkService(db, vm["default-release-cover,"].as<std::string>(), vm["default-artist-image"].as<std::string>()) };
|
||||||
|
|
||||||
coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as<unsigned>()));
|
coverArtService->setJpegQuality(config->getULong("cover-jpeg-quality", vm["quality"].as<unsigned>()));
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "core/Random.hpp"
|
#include "core/Random.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/StreamLogger.hpp"
|
#include "core/StreamLogger.hpp"
|
||||||
|
#include "core/SystemPaths.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
@@ -177,7 +178,7 @@ int main(int argc, char* argv[])
|
|||||||
const GeneratorParameters defaultParams;
|
const GeneratorParameters defaultParams;
|
||||||
|
|
||||||
program_options::options_description options{ "Options" };
|
program_options::options_description options{ "Options" };
|
||||||
options.add_options()("conf,c", program_options::value<std::string>()->default_value("/etc/lms.conf"), "lms config file")("media-library-count", program_options::value<unsigned>()->default_value(defaultParams.mediaLibraryCount), "Number of media libraries to use")("release-count-per-batch", program_options::value<unsigned>()->default_value(defaultParams.releaseCountPerBatch), "Number of releases to generate before committing transaction")("release-count", program_options::value<unsigned>()->default_value(defaultParams.releaseCount), "Number of releases to generate")("track-count-per-release", program_options::value<unsigned>()->default_value(defaultParams.trackCountPerRelease), "Number of tracks per release")("compilation-ratio", program_options::value<float>()->default_value(defaultParams.compilationRatio), "Compilation ratio (compilation means all tracks have a different artist)")("track-path", program_options::value<std::string>()->required(), "Path of a valid track file, that will be used for all generated tracks")("genre-count", program_options::value<unsigned>()->default_value(defaultParams.genreCount), "Number of genres to generate")("genre-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.genreCountPerTrack), "Number of genres to assign to each track")("mood-count", program_options::value<unsigned>()->default_value(defaultParams.moodCount), "Number of moods to generate")("mood-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.moodCountPerTrack), "Number of moods to assign to each track")("help,h", "produce help message");
|
options.add_options()("conf,c", program_options::value<std::string>()->default_value(core::sysconfDirectory / "lms.conf"), "lms config file")("media-library-count", program_options::value<unsigned>()->default_value(defaultParams.mediaLibraryCount), "Number of media libraries to use")("release-count-per-batch", program_options::value<unsigned>()->default_value(defaultParams.releaseCountPerBatch), "Number of releases to generate before committing transaction")("release-count", program_options::value<unsigned>()->default_value(defaultParams.releaseCount), "Number of releases to generate")("track-count-per-release", program_options::value<unsigned>()->default_value(defaultParams.trackCountPerRelease), "Number of tracks per release")("compilation-ratio", program_options::value<float>()->default_value(defaultParams.compilationRatio), "Compilation ratio (compilation means all tracks have a different artist)")("track-path", program_options::value<std::string>()->required(), "Path of a valid track file, that will be used for all generated tracks")("genre-count", program_options::value<unsigned>()->default_value(defaultParams.genreCount), "Number of genres to generate")("genre-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.genreCountPerTrack), "Number of genres to assign to each track")("mood-count", program_options::value<unsigned>()->default_value(defaultParams.moodCount), "Number of moods to generate")("mood-count-per-track", program_options::value<unsigned>()->default_value(defaultParams.moodCountPerTrack), "Number of moods to assign to each track")("help,h", "produce help message");
|
||||||
|
|
||||||
program_options::variables_map vm;
|
program_options::variables_map vm;
|
||||||
program_options::store(program_options::parse_command_line(argc, argv, options), vm);
|
program_options::store(program_options::parse_command_line(argc, argv, options), vm);
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
#include <boost/program_options.hpp>
|
#include <boost/program_options.hpp>
|
||||||
|
|
||||||
|
#include "core/EnumSet.hpp"
|
||||||
#include "core/StreamLogger.hpp"
|
#include "core/StreamLogger.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
#include "metadata/Exception.hpp"
|
#include "metadata/Exception.hpp"
|
||||||
#include "metadata/IParser.hpp"
|
#include "metadata/IParser.hpp"
|
||||||
|
|
||||||
@@ -241,7 +243,8 @@ int main(int argc, char* argv[])
|
|||||||
options.add_options()
|
options.add_options()
|
||||||
("help,h", "Display this help message")
|
("help,h", "Display this help message")
|
||||||
("tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Tag delimiters (multiple allowed)")
|
("tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Tag delimiters (multiple allowed)")
|
||||||
("artist-tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Artist tag delimiters (multiple allowed)");
|
("artist-tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Artist tag delimiters (multiple allowed)")
|
||||||
|
("parser", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{ "taglib" }, "[taglib]"), "Parser to be used (value can be \"taglib\" or \"ffmpeg\")");
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
program_options::options_description hiddenOptions{ "Hidden options" };
|
program_options::options_description hiddenOptions{ "Hidden options" };
|
||||||
@@ -274,13 +277,39 @@ int main(int argc, char* argv[])
|
|||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vm.count("file"))
|
if (vm.count("file") == 0)
|
||||||
{
|
{
|
||||||
std::cout << "NO INPUT FILE!" << std::endl;
|
std::cerr << "No input file provided" << std::endl;
|
||||||
displayHelp(std::cerr);
|
displayHelp(std::cerr);
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Parser
|
||||||
|
{
|
||||||
|
Taglib,
|
||||||
|
Ffmpeg,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vm.count("parser") == 0)
|
||||||
|
{
|
||||||
|
std::cerr << "You must specify at least one parser" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::EnumSet<Parser> parsers;
|
||||||
|
for (const std::string& strParser : vm["parser"].as<std::vector<std::string>>())
|
||||||
|
{
|
||||||
|
if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "taglib"))
|
||||||
|
parsers.insert(Parser::Taglib);
|
||||||
|
else if (core::stringUtils::stringCaseInsensitiveEqual(strParser, "ffmpeg"))
|
||||||
|
parsers.insert(Parser::Ffmpeg);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Invalid parser name '" << strParser << "'" << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto& inputFiles{ vm["file"].as<std::vector<std::string>>() };
|
const auto& inputFiles{ vm["file"].as<std::vector<std::string>>() };
|
||||||
const auto& tagDelimiters{ vm["tag-delimiter"].as<std::vector<std::string>>() };
|
const auto& tagDelimiters{ vm["tag-delimiter"].as<std::vector<std::string>>() };
|
||||||
const auto& artistTagDelimiters{ vm["artist-tag-delimiter"].as<std::vector<std::string>>() };
|
const auto& artistTagDelimiters{ vm["artist-tag-delimiter"].as<std::vector<std::string>>() };
|
||||||
@@ -304,34 +333,40 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
std::cout << "Parsing file '" << file << "'" << std::endl;
|
std::cout << "Parsing file '" << file << "'" << std::endl;
|
||||||
|
|
||||||
try
|
if (parsers.contains(Parser::Ffmpeg))
|
||||||
{
|
{
|
||||||
std::cout << "Using av:" << std::endl;
|
try
|
||||||
|
{
|
||||||
|
std::cout << "Using Ffmpeg:" << std::endl;
|
||||||
|
|
||||||
auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) };
|
auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) };
|
||||||
parser->setArtistTagDelimiters(artistTagDelimiters);
|
parser->setArtistTagDelimiters(artistTagDelimiters);
|
||||||
parser->setDefaultTagDelimiters(tagDelimiters);
|
parser->setDefaultTagDelimiters(tagDelimiters);
|
||||||
|
|
||||||
parse(*parser, file);
|
parse(*parser, file);
|
||||||
}
|
}
|
||||||
catch (metadata::Exception& e)
|
catch (metadata::Exception& e)
|
||||||
{
|
{
|
||||||
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (parsers.contains(Parser::Taglib))
|
||||||
{
|
{
|
||||||
std::cout << "Using TagLib:" << std::endl;
|
try
|
||||||
|
{
|
||||||
|
std::cout << "Using TagLib:" << std::endl;
|
||||||
|
|
||||||
auto parser{ metadata::createParser(metadata::ParserBackend::TagLib, metadata::ParserReadStyle::Accurate) };
|
auto parser{ metadata::createParser(metadata::ParserBackend::TagLib, metadata::ParserReadStyle::Accurate) };
|
||||||
parser->setArtistTagDelimiters(artistTagDelimiters);
|
parser->setArtistTagDelimiters(artistTagDelimiters);
|
||||||
parser->setDefaultTagDelimiters(tagDelimiters);
|
parser->setDefaultTagDelimiters(tagDelimiters);
|
||||||
|
|
||||||
parse(*parser, file);
|
parse(*parser, file);
|
||||||
}
|
}
|
||||||
catch (metadata::Exception& e)
|
catch (metadata::Exception& e)
|
||||||
{
|
{
|
||||||
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
std::cerr << "Parsing failed: " << e.what() << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/StreamLogger.hpp"
|
#include "core/StreamLogger.hpp"
|
||||||
|
#include "core/SystemPaths.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
@@ -132,7 +133,7 @@ int main(int argc, char* argv[])
|
|||||||
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
|
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
|
||||||
|
|
||||||
po::options_description desc{ "Allowed options" };
|
po::options_description desc{ "Allowed options" };
|
||||||
desc.add_options()("help,h", "print usage message")("conf,c", po::value<std::string>()->default_value("/etc/lms.conf"), "LMS config file")("artists,a", "Display recommendation for artists")("releases,r", "Display recommendation for releases")("tracks,t", "Display recommendation for tracks")("max,m", po::value<unsigned>()->default_value(3), "Max similarity result count");
|
desc.add_options()("help,h", "print usage message")("conf,c", po::value<std::string>()->default_value(core::sysconfDirectory / "lms.conf"), "LMS config file")("artists,a", "Display recommendation for artists")("releases,r", "Display recommendation for releases")("tracks,t", "Display recommendation for tracks")("max,m", po::value<unsigned>()->default_value(3), "Max similarity result count");
|
||||||
|
|
||||||
po::variables_map vm;
|
po::variables_map vm;
|
||||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||||
|
|||||||
Reference in New Issue
Block a user