Merge branch 'develop' for release v3.59.0

This commit is contained in:
emeric
2024-10-11 21:21:56 +02:00
83 changed files with 1982 additions and 1001 deletions
+6 -4
View File
@@ -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.
## 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_:
```
server {
@@ -147,10 +150,9 @@ server {
keepalive_timeout 10m;
location / {
proxy_set_header Client-IP $remote_addr;
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_pass http://localhost:5082/;
+3 -3
View File
@@ -43,15 +43,15 @@ _LMS_ primarily relies on tags to organize your music collection but also suppor
### 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.
__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
_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
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(albumartistssort,%_albumartists_sort%)
+1 -1
View File
@@ -29,7 +29,7 @@
${</if-has-non-release-tracks>}
${<if-has-similar-artists>}
<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>}
</message>
+9 -2
View File
@@ -14,11 +14,18 @@
</message>
<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 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}
</message>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+9
View File
@@ -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

+8
View File
@@ -110,6 +110,7 @@
<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.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-removed-files">Checking for removed files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
@@ -276,18 +277,24 @@
<message id="Lms.PlayHistory.playhistory">Play History</message>
<!--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-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.listenbrainz">ListenBrainz</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.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-format">Default transcoding output format</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.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.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-must-fill-old-password">Old password must be filled in</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.track-artists">Track artists</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-output-bitrate">Transcoding output bitrate</message>
<message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
+8
View File
@@ -110,6 +110,7 @@
<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.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-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>
@@ -276,18 +277,24 @@
<message id="Lms.PlayHistory.playhistory">Historique de lecture</message>
<!--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-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.listenbrainz">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.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-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.enable-transcoding-by-default">Activer par défaut le transcodage</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.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-must-fill-old-password">L'ancien mot de passe doit être renseigné</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.track-artists">Tous les artistes de piste</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-output-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
+8
View File
@@ -110,6 +110,7 @@
<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.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-removed-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
@@ -276,18 +277,24 @@
<message id="Lms.PlayHistory.playhistory">Storico riproduzione</message>
<!--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-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.listenbrainz">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.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-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.enable-transcoding-by-default">Abilita transcodifica di default</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.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-must-fill-old-password">Devi inserire la password attuale</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.track-artists">Artisti tracce</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-output-bitrate">Bitrate transcodifica</message>
<message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
+8
View File
@@ -119,6 +119,7 @@
<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.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">
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
@@ -302,18 +303,24 @@
<message id="Lms.PlayHistory.playhistory">Historia odtwarzania</message>
<!--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-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.listenbrainz">ListenBrainz</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.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-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.enable-transcoding-by-default">Aktywuj domyślnie transkodowanie</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.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-must-fill-old-password">Należy podać stare hasło</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.track-artists">Artyści ze ścieżek</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-output-bitrate">Przepływność transkodowania</message>
<message id="Lms.Settings.transcoding-output-format">Format wyjściowy transkodowania</message>
+10 -1
View File
@@ -80,6 +80,7 @@
<message id="Lms.Admin.Database.update-period">更新周期</message>
<message id="Lms.Admin.Database.update-start-time">更新开始时间</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-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-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
@@ -276,6 +278,7 @@
<message id="Lms.PlayHistory.playhistory">播放历史</message>
<!--Settings-->
<message id="Lms.Settings.audio">音频</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.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.password-bad">无效密码</message>
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</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.track-artists">音轨歌手</message>
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
<message id="Lms.Settings.transcoding">转码</message>
<message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
<message id="Lms.Settings.transcoding-output-format">转码格式</message>
+14 -3
View File
@@ -5,10 +5,21 @@
<message id="Lms.Settings.template">
<form class="row g-3">
<legend>${tr:Lms.Settings.audio}</legend>
<div class="alert alert-info">
${tr:Lms.Settings.audio-settings-are-local}
<legend>${tr:Lms.Settings.user-interface}</legend>
<div class="col-12">
<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>
<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">
<label class="form-label" for="${id:transcoding-mode}">
${tr:Lms.Settings.transcoding-mode}
+8 -4
View File
@@ -19,6 +19,13 @@ db-show-queries = false;
listen-port = 5082;
listen-addr = "0.0.0.0";
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
tls-enable = false;
@@ -74,9 +81,6 @@ api-open-subsonic-disabled-clients = ("DSub");
# Turn on this option to allow the demo account creation/use
demo = false;
# Max external cover file size in MBytes
cover-max-file-size = 10;
# Max cover cache size in MBytes
cover-max-cache-size = 30;
@@ -84,7 +88,7 @@ cover-max-cache-size = 30;
cover-jpeg-quality = 75;
# 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)
# 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" };
}
+1 -2
View File
@@ -277,7 +277,7 @@ namespace lms::db
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
@@ -379,5 +379,4 @@ namespace lms::db
{
_image = getDboPtr(image);
}
} // namespace lms::db
+2 -1
View File
@@ -23,6 +23,7 @@
#include "database/Artist.hpp"
#include "database/Directory.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "IdTypeTraits.hpp"
@@ -40,7 +41,7 @@ namespace lms::db
if (params.directory.isValid())
query.where("i.directory_id = ?").bind(params.directory);
if (!params.fileStem.empty())
query.where("i.stem = ?").bind(params.fileStem);
query.where("i.stem = ? COLLATE NOCASE").bind(params.fileStem);
return query;
}
+122 -2
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 67 };
static constexpr Version LMS_DATABASE_VERSION{ 70 };
}
VersionInfo::VersionInfo()
@@ -761,9 +761,126 @@ SELECT
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)
{
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() };
@@ -804,6 +921,9 @@ SELECT
{ 64, migrateFromV64 },
{ 65, migrateFromV65 },
{ 66, migrateFromV66 },
{ 67, migrateFromV67 },
{ 68, migrateFromV68 },
{ 69, migrateFromV69 },
};
bool migrationPerformed{};
+40 -1
View File
@@ -24,6 +24,8 @@
#include "core/ILogger.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
@@ -242,6 +244,11 @@ namespace lms::db
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)
{
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));
}
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)
: _name{ name }
{
@@ -272,6 +288,11 @@ namespace lms::db
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)
{
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));
}
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)
: _name{ std::string(name, 0, _maxNameLength) }
, _MBID{ MBID ? MBID->getAsString() : "" }
@@ -540,6 +570,11 @@ namespace lms::db
return utils::fetchQueryResults<Release::pointer>(query);
}
ObjectPtr<Image> Release::getImage() const
{
return ObjectPtr<Image>{ _image };
}
void Release::clearLabels()
{
_labels.clear();
@@ -560,6 +595,11 @@ namespace lms::db
_releaseTypes.insert(getDboPtr(releaseType));
}
void Release::setImage(ObjectPtr<Image> image)
{
_image = getDboPtr(image);
}
bool Release::hasVariousArtists() const
{
// TODO optimize
@@ -665,5 +705,4 @@ namespace lms::db
return res;
}
} // namespace lms::db
+5 -5
View File
@@ -179,6 +179,7 @@ namespace lms::db
auto transaction{ createWriteTransaction() };
_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_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_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_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_idx ON image(directory_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_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_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)");
@@ -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 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_name_idx ON release(name)");
_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_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_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_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_track_idx ON track_bookmark(user_id,track_id)");
+3
View File
@@ -157,6 +157,9 @@ namespace lms::db
if (params.directory.isValid())
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)
{
case TrackSortMethod::None:
@@ -162,7 +162,7 @@ namespace lms::db
Wt::Dbo::field(a, _sortName, "sort_name");
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, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
}
@@ -179,7 +179,7 @@ namespace lms::db
std::string _sortName;
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<StarredArtist>> _starredArtists; // starred entries for this artist
};
+5 -4
View File
@@ -25,7 +25,6 @@
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/ArtistId.hpp"
#include "database/DirectoryId.hpp"
#include "database/ImageId.hpp"
#include "database/Object.hpp"
@@ -35,6 +34,7 @@ namespace lms::db
{
class Artist;
class Directory;
class Release;
class Session;
class Image final : public Object<Image, ImageId>
@@ -87,7 +87,6 @@ namespace lms::db
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setWidth(std::size_t width) { _width = width; }
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); }
template<class Action>
@@ -101,7 +100,8 @@ namespace lms::db
Wt::Dbo::field(a, _width, "width");
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);
}
@@ -117,7 +117,8 @@ namespace lms::db
int _width{};
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;
};
} // namespace lms::db
@@ -47,6 +47,7 @@ namespace lms::db
class Artist;
class Cluster;
class ClusterType;
class Image;
class Release;
class Session;
class Track;
@@ -56,8 +57,11 @@ namespace lms::db
{
public:
Label() = default;
static std::size_t getCount(Session& session);
static pointer find(Session& session, LabelId id);
static pointer find(Session& session, std::string_view name);
static RangeResults<LabelId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors
std::string_view getName() const { return _name; }
@@ -84,8 +88,11 @@ namespace lms::db
{
public:
ReleaseType() = default;
static std::size_t getCount(Session& session);
static pointer find(Session& session, ReleaseTypeId id);
static pointer find(Session& session, std::string_view name);
static RangeResults<ReleaseTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
// Accessors
std::string_view getName() const { return _name; }
@@ -245,6 +252,7 @@ namespace lms::db
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
ObjectPtr<Image> getImage() const;
// Setters
void setName(std::string_view name) { _name = name; }
@@ -258,6 +266,7 @@ namespace lms::db
void clearReleaseTypes();
void addLabel(ObjectPtr<Label> releaseType);
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
void setImage(ObjectPtr<Image> image);
// Get the artists of this release
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::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, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
}
@@ -300,6 +310,7 @@ namespace lms::db
std::string _artistDisplayName;
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<Label>> _labels;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
@@ -83,6 +83,7 @@ namespace lms::db
std::optional<int> discNumber; // matching this disc number
MediaLibraryId mediaLibrary; // If set, tracks in this library
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)
{
@@ -172,6 +173,11 @@ namespace lms::db
directory = _directory;
return *this;
}
FindParameters& setHasEmbeddedImage(std::optional<bool> _hasEmbeddedImage)
{
hasEmbeddedImage = _hasEmbeddedImage;
return *this;
}
};
struct PathResult
+5 -1
View File
@@ -75,6 +75,7 @@ namespace lms::db
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
static inline constexpr ReleaseSortMethod _defaultUIArtistReleaseSortMethod{ ReleaseSortMethod::OriginalDateDesc };
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
@@ -106,6 +107,7 @@ namespace lms::db
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
@@ -120,6 +122,7 @@ namespace lms::db
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
UITheme getUITheme() const { return _uiTheme; }
ReleaseSortMethod getUIArtistReleaseSortMethod() const { return _uiArtistReleaseSortMethod; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
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, _subsonicArtistListMode, "subsonic_artist_list_mode");
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, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
@@ -156,6 +160,7 @@ namespace lms::db
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme };
ReleaseSortMethod _uiArtistReleaseSortMethod{ _defaultUIArtistReleaseSortMethod };
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
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<UIState>> _uiStates;
};
} // namespace lms::db
+28
View File
@@ -19,8 +19,12 @@
#include "Common.hpp"
#include "database/Image.hpp"
namespace lms::db::tests
{
using ScopedImage = ScopedEntity<db::Image>;
TEST_F(DatabaseFixture, Artist)
{
{
@@ -697,4 +701,28 @@ namespace lms::db::tests
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
+2
View File
@@ -80,10 +80,12 @@ namespace lms::db::tests
EXPECT_EQ(Artist::getCount(session), 0);
EXPECT_EQ(Cluster::getCount(session), 0);
EXPECT_EQ(ClusterType::getCount(session), 0);
EXPECT_EQ(Label::getCount(session), 0);
EXPECT_EQ(Listen::getCount(session), 0);
EXPECT_EQ(Image::getCount(session), 0);
EXPECT_EQ(MediaLibrary::getCount(session), 0);
EXPECT_EQ(Release::getCount(session), 0);
EXPECT_EQ(ReleaseType::getCount(session), 0);
EXPECT_EQ(StarredArtist::getCount(session), 0);
EXPECT_EQ(StarredRelease::getCount(session), 0);
EXPECT_EQ(StarredTrack::getCount(session), 0);
+102
View File
@@ -19,8 +19,11 @@
#include "Common.hpp"
#include "database/Image.hpp"
namespace lms::db::tests
{
using ScopedImage = ScopedEntity<db::Image>;
using ScopedLabel = ScopedEntity<db::Label>;
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)
{
{
@@ -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)
{
ScopedRelease release{ session, "MyRelease" };
@@ -963,4 +1041,28 @@ namespace lms::db::tests
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
+26 -11
View File
@@ -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; });
}
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)
return artists.front().name;
artistDisplayName = artists.front().name;
else if (artists.size() > 1)
{
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; });
// 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
if (artistTag && !strIsContainingAny(*artistTag, artistTagDelimiters) && strIsMatchingArtistNames(*artistTag, artistNames))
return *artistTag;
else
return core::stringUtils::joinStrings(artistNames, ", ");
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
{
if (artistsTag.size() == artists.size())
{
// 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
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
@@ -339,7 +354,7 @@ namespace lms::metadata
track.medium = getMedium(tagReader);
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.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->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->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->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
+56
View File
@@ -273,6 +273,45 @@ namespace lms::metadata
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)
{
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
}
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)
{
const TestTagReader testTags{
+1 -1
View File
@@ -1,5 +1,5 @@
add_subdirectory(artwork)
add_subdirectory(auth)
add_subdirectory(cover)
add_subdirectory(feedback)
add_subdirectory(recommendation)
add_subdirectory(scanner)
+29
View File
@@ -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
@@ -34,22 +34,28 @@ namespace lms::db
namespace lms::cover
{
class ICoverService
class IArtworkService
{
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> 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> getArtistImage(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 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
-29
View File
@@ -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
+1
View File
@@ -4,6 +4,7 @@ add_library(lmsscanner SHARED
impl/ScannerService.cpp
impl/ScannerStats.cpp
impl/ScanStepAssociateArtistImages.cpp
impl/ScanStepAssociateReleaseImages.cpp
impl/ScanStepCheckForDuplicatedFiles.cpp
impl/ScanStepCheckForRemovedFiles.cpp
impl/ScanStepCompact.cpp
@@ -41,7 +41,7 @@ namespace lms::scanner
namespace
{
constexpr std::size_t readBatchSize{ 100 };
constexpr std::size_t writeBatchSize{ 10 };
constexpr std::size_t writeBatchSize{ 20 };
struct ArtistImageAssociation
{
@@ -170,15 +170,15 @@ namespace lms::scanner
void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations)
{
if (imageAssociations.empty())
return;
auto transaction{ session.createWriteTransaction() };
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
while (!imageAssociations.empty())
{
updateArtistImage(session, imageAssociations.front());
imageAssociations.pop_front();
auto transaction{ session.createWriteTransaction() };
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);
removeOrphanedArtists(context);
removeOrphanedReleases(context);
removeOrphanedReleaseTypes(context);
removeOrphanedLabels(context);
removeOrphanedDirectories(context);
}
@@ -64,6 +66,18 @@ namespace lms::scanner
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)
{
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned directories...");
@@ -39,6 +39,8 @@ namespace lms::scanner
void removeOrphanedClusterTypes(ScanContext& context);
void removeOrphanedArtists(ScanContext& context);
void removeOrphanedReleases(ScanContext& context);
void removeOrphanedReleaseTypes(ScanContext& context);
void removeOrphanedLabels(ScanContext& context);
void removeOrphanedDirectories(ScanContext& context);
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;
@@ -265,12 +275,36 @@ namespace lms::scanner
}
else if (releaseInfo.name.empty())
{
// No release name (only mbid) -> nothing to de
// No release name (only mbid) -> nothing to do
return release;
}
// 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)
{
Release::FindParameters params;
@@ -285,30 +319,7 @@ namespace lms::scanner
if (candidateRelease->getMBID().has_value())
return;
// TODO: add more criterias?
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; }))
if (!isReleaseMatching(candidateRelease, releaseInfo))
return;
release = candidateRelease;
@@ -737,7 +748,7 @@ namespace lms::scanner
track.modify()->setScanVersion(_settings.scanVersion);
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
track.modify()->setRelease({});
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
@@ -32,6 +32,7 @@
#include "image/Image.hpp"
#include "ScanStepAssociateArtistImages.hpp"
#include "ScanStepAssociateReleaseImages.hpp"
#include "ScanStepCheckForDuplicatedFiles.hpp"
#include "ScanStepCheckForRemovedFiles.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<ScanStepUpdateLibraryFields>(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<ScanStepCompact>(params));
_scanSteps.push_back(std::make_unique<ScanStepOptimize>(params));
@@ -62,6 +62,7 @@ namespace lms::scanner
enum class ScanStep
{
AssociateArtistImages,
AssociateReleaseImages,
CheckForDuplicatedFiles,
CheckForRemovedFiles,
ComputeClusterStats,
+1 -1
View File
@@ -48,7 +48,7 @@ target_link_libraries(lmssubsonic PRIVATE
lmsrecommendation
lmsscanner
lmsscrobbling
lmsservice-cover
lmsartwork
lmscore
std::filesystem
)
+2 -2
View File
@@ -282,7 +282,7 @@ namespace lms::api::subsonic
SubsonicResource::SubsonicResource(db::Db& db)
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
, _defaultCoverClients{ readDefaultCoverClients() }
, _defaultReleaseCoverClients{ readDefaultCoverClients() }
, _db{ db }
{
}
@@ -410,7 +410,7 @@ namespace lms::api::subsonic
const ClientInfo clientInfo{ getClientInfo(request) };
const db::UserId userId{ authenticateUser(request, clientInfo) };
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 };
db::User::pointer user;
+1 -1
View File
@@ -54,7 +54,7 @@ namespace lms::api::subsonic
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients;
const std::unordered_set<std::string> _defaultCoverClients;
const std::unordered_set<std::string> _defaultReleaseCoverClients;
db::Db& _db;
};
@@ -32,7 +32,7 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "services/cover/ICoverService.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
@@ -267,14 +267,14 @@ namespace lms::api::subsonic
std::shared_ptr<image::IEncodedImage> cover;
if (trackId)
cover = core::Service<cover::ICoverService>::get()->getFromTrack(*trackId, size);
cover = core::Service<cover::IArtworkService>::get()->getTrackImage(*trackId, size);
else if (releaseId)
cover = core::Service<cover::ICoverService>::get()->getFromRelease(*releaseId, size);
cover = core::Service<cover::IArtworkService>::get()->getReleaseCover(*releaseId, size);
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)
cover = core::Service<cover::ICoverService>::get()->getDefaultSvgCover();
cover = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
if (!cover)
{
+17 -1
View File
@@ -25,7 +25,9 @@
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
@@ -82,7 +84,21 @@ namespace lms::api::subsonic
}
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() })
albumNode.setAttribute("year", *year);
+1 -1
View File
@@ -94,7 +94,7 @@ namespace lms::api::subsonic
artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName());
if (const db::Image::pointer artistImage{ artist->getImage() })
if (artist->getImage())
artistNode.setAttribute("coverArt", idToString(artist->getId()));
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
+10 -3
View File
@@ -31,7 +31,7 @@ namespace lms::api::subsonic
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;
@@ -43,8 +43,15 @@ namespace lms::api::subsonic
playlistNode.setAttribute("created", reportedDummyDate);
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
if (const auto entry{ tracklist->getEntry(0) })
playlistNode.setAttribute("coverArt", idToString(entry->getTrack()->getId()));
db::Track::FindParameters params;
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;
}
+2 -1
View File
@@ -28,6 +28,7 @@
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
@@ -110,7 +111,7 @@ namespace lms::api::subsonic
if (track->hasCover())
trackResponse.setAttribute("coverArt", idToString(track->getId()));
else if (release)
else if (release && release->getImage())
trackResponse.setAttribute("coverArt", idToString(release->getId()));
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
+3 -2
View File
@@ -12,6 +12,7 @@ add_executable(lms
ui/PlayQueue.cpp
ui/SettingsView.cpp
ui/State.cpp
ui/Tooltip.cpp
ui/Utils.cpp
ui/admin/InitWizardView.cpp
ui/admin/MediaLibrariesView.cpp
@@ -50,7 +51,7 @@ add_executable(lms
ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodingResource.cpp
ui/resource/CoverResource.cpp
ui/resource/ArtworkResource.cpp
ui/resource/DownloadResource.cpp
)
@@ -69,7 +70,7 @@ target_link_libraries(lms PRIVATE
lmsrecommendation
lmsscanner
lmsscrobbling
lmsservice-cover
lmsartwork
lmssubsonic
lmscore
)
+172 -147
View File
@@ -30,14 +30,15 @@
#include "core/ITraceLogger.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "core/SystemPaths.hpp"
#include "core/WtLogger.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "image/Image.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IEnvService.hpp"
#include "services/auth/IPasswordService.hpp"
#include "services/cover/ICoverService.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IPlaylistGeneratorService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
@@ -50,180 +51,204 @@
namespace lms
{
std::size_t getThreadCount()
namespace
{
const unsigned long configHttpServerThreadCount{ core::Service<core::IConfig>::get()->getULong("http-server-thread-count", 0) };
// 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))
std::size_t getThreadCount()
{
args.push_back("--https-port=" + std::to_string(core::Service<core::IConfig>::get()->getULong("listen-port", 5082)));
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") });
const unsigned long configHttpServerThreadCount{ core::Service<core::IConfig>::get()->getULong("http-server-thread-count", 0) };
// 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());
}
if (!wtAccessLogFilePath.empty())
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));
core::logging::Severity getLogMinSeverity()
{
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::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::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
if (!oss)
throw core::LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" };
std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") };
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{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
throw core::LmsException{ "Invalid config value for 'tracing-level'" };
}
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)
{
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();
});
} };
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") };
scanner.getEvents().scanAborted.connect([&] {
postAll(server, [] {
LmsApp->getScannerEvents().scanAborted.emit();
LmsApp->triggerUpdate();
});
});
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());
scanner.getEvents().scanStarted.connect([&] {
postAll(server, [] {
LmsApp->getScannerEvents().scanStarted.emit();
LmsApp->triggerUpdate();
});
});
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)));
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) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanComplete.emit(stats);
LmsApp->triggerUpdate();
});
});
if (!wtAccessLogFilePath.empty())
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
scanner.getEvents().scanInProgress.connect([&](const scanner::ScanStepStats& stats) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanInProgress.emit(stats);
LmsApp->triggerUpdate();
});
});
args.push_back("--threads=" + std::to_string(getThreadCount()));
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) {
postAll(server, [=] {
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
LmsApp->triggerUpdate();
// 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));
// 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[])
{
std::filesystem::path configFilePath{ "/etc/lms.conf" };
std::filesystem::path configFilePath{ core::sysconfDirectory / "lms.conf" };
int res{ EXIT_FAILURE };
assert(argc > 0);
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)
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)
{
std::cerr << "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";
displayUsage(std::cerr);
return EXIT_FAILURE;
}
@@ -315,7 +340,7 @@ namespace lms
throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
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::IPlaylistGeneratorService> playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
core::Service<scanner::IScannerService> scannerService{ scanner::createScannerService(database) };
@@ -323,7 +348,7 @@ namespace lms
scannerService->getEvents().scanComplete.connect([&] {
// 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)
coverService->flushCache();
artworkService->flushCache();
database.getTLSSession().refreshTracingLoggerStats();
});
+3 -3
View File
@@ -37,9 +37,9 @@
#include "database/Session.hpp"
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "services/artwork/IArtworkService.hpp"
#include "services/auth/IEnvService.hpp"
#include "services/auth/IPasswordService.hpp"
#include "services/cover/ICoverService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "Auth.hpp"
@@ -61,9 +61,9 @@
#include "common/Template.hpp"
#include "explore/Explore.hpp"
#include "explore/Filters.hpp"
#include "resource/ArtworkResource.hpp"
#include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodingResource.hpp"
#include "resource/CoverResource.hpp"
namespace lms::ui
{
@@ -400,7 +400,7 @@ namespace lms::ui
{
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("updateActiveNav",
+3 -3
View File
@@ -42,7 +42,7 @@ namespace lms::db
namespace lms::ui
{
class CoverResource;
class ArtworkResource;
class LmsApplicationException;
class MediaPlayer;
class PlayQueue;
@@ -60,7 +60,7 @@ namespace lms::ui
static LmsApplication* instance();
// Session application data
std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; }
std::shared_ptr<ArtworkResource> getArtworkResource() { return _artworkResource; }
db::Db& getDb();
db::Session& getDbSession(); // always thread safe
@@ -115,7 +115,7 @@ namespace lms::ui
bool strongAuth{};
};
std::optional<UserAuthInfo> _user;
std::shared_ptr<CoverResource> _coverResource;
std::shared_ptr<ArtworkResource> _artworkResource;
MediaPlayer* _mediaPlayer{};
PlayQueue* _playQueue{};
NotificationContainer* _notificationContainer{};
+5 -5
View File
@@ -37,9 +37,9 @@
#include "LmsApplication.hpp"
#include "Utils.hpp"
#include "resource/ArtworkResource.hpp"
#include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodingResource.hpp"
#include "resource/CoverResource.hpp"
namespace lms::ui
{
@@ -255,13 +255,13 @@ namespace lms::ui
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
<< " artwork: ["
<< " { src: \"" << LmsApp->getCoverResource()->getTrackUrl(trackId, CoverResource::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::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
<< " { src: \"" << LmsApp->getArtworkResource()->getTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
<< " ]"
<< "};";
// 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<CoverResource::Size>>(CoverResource::Size::Large) == 512);
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Small) == 128);
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
_title->setTextFormat(Wt::TextFormat::Plain);
+8 -10
View File
@@ -485,22 +485,20 @@ namespace lms::ui
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists));
}
const auto release{ track->getRelease() };
if (release)
auto image{ utils::createTrackImage(track->getId(), ArtworkResource::Size::Small) };
image->addStyleClass("Lms-cover-track");
if (const auto release{ track->getRelease() })
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", utils::createReleaseAnchor(release));
{
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Small) };
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
anchor->setImage(std::move(cover));
}
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
image->addStyleClass("Lms-cover-anchor"); // HACK
anchor->setImage(std::move(image));
}
else
{
auto cover{ entry->bindWidget<Wt::WImage>("cover", utils::createCover(track->getId(), CoverResource::Size::Small)) };
cover->addStyleClass("Lms-cover-track");
entry->bindWidget("cover", std::move(image));
}
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
+50 -18
View File
@@ -37,6 +37,7 @@
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "Tooltip.hpp"
#include "common/DoubleValidator.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/PasswordValidator.hpp"
@@ -51,6 +52,7 @@ namespace lms::ui
{
public:
// 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 TranscodeFormatField{ "transcoding-output-format" };
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 PasswordConfirmField{ "password-confirm" };
using ArtistReleaseSortMethodModel = ValueStringModel<db::ReleaseSortMethod>;
using TranscodingModeModel = ValueStringModel<MediaPlayer::Settings::Transcoding::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
using FeedbackBackendModel = ValueStringModel<FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<ScrobblingBackend>;
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword)
: _authPasswordService{ authPasswordService }
@@ -79,6 +82,7 @@ namespace lms::ui
{
initializeModels();
addField(ArtistReleaseSortMethodField);
addField(TranscodingModeField);
addField(TranscodeBitrateField);
addField(TranscodeFormatField);
@@ -106,6 +110,7 @@ namespace lms::ui
addField(PasswordConfirmField);
}
setValidator(ArtistReleaseSortMethodField, createMandatoryValidator());
setValidator(TranscodingModeField, createMandatoryValidator());
setValidator(TranscodeBitrateField, createMandatoryValidator());
setValidator(TranscodeFormatField, createMandatoryValidator());
@@ -122,6 +127,7 @@ namespace lms::ui
loadData();
}
std::shared_ptr<ArtistReleaseSortMethodModel> getArtistReleaseSortMethodModel() { return _artistReleaseSortMethodModel; }
std::shared_ptr<TranscodingModeModel> getTranscodingModeModel() { return _transcodingModeModeModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputBitrateModel() { return _transcodingOutputBitrateModel; }
std::shared_ptr<Wt::WAbstractItemModel> getTranscodingOutputFormatModel() { return _transcodingOutputFormatModel; }
@@ -136,6 +142,12 @@ namespace lms::ui
User::pointer user{ LmsApp->getUser() };
{
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromString(valueText(ArtistReleaseSortMethodField)) };
if (artistReleaseSortMethodRow)
user.modify()->setUIArtistReleaseSortMethod(_artistReleaseSortMethodModel->getValue(*artistReleaseSortMethodRow));
}
{
MediaPlayer::Settings settings;
@@ -204,6 +216,12 @@ namespace lms::ui
User::pointer user{ LmsApp->getUser() };
{
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) };
if (artistReleaseSortMethodRow)
setValue(ArtistReleaseSortMethodField, _artistReleaseSortMethodModel->getString(*artistReleaseSortMethodRow));
}
{
const auto settings{ *LmsApp->getMediaPlayer().getSettings() };
@@ -315,6 +333,13 @@ namespace lms::ui
private:
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->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);
@@ -355,6 +380,7 @@ namespace lms::ui
auth::IPasswordService* _authPasswordService{};
bool _withOldPassword{};
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
std::shared_ptr<ValueStringModel<TranscodingOutputFormat>> _transcodingOutputFormatModel;
@@ -425,12 +451,25 @@ namespace lms::ui
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
{
// Transcode
auto transcodingMode{ std::make_unique<Wt::WComboBox>() };
auto* transcodingModeRaw{ transcodingMode.get() };
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));
// Format
@@ -443,17 +482,15 @@ namespace lms::ui
transcodingOutputBitrate->setModel(model->getTranscodingOutputBitrateModel());
t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodingOutputBitrate));
transcodingModeRaw->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);
// Replay gain mode
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
replayGainMode->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());
});
// Replay gain mode
auto replayGainMode{ std::make_unique<Wt::WComboBox>() };
auto* replayGainModeRaw{ replayGainMode.get() };
replayGainMode->setModel(model->getReplayGainModeModel());
t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode));
@@ -467,13 +504,6 @@ namespace lms::ui
replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
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)
{
model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true);
@@ -573,6 +603,8 @@ namespace lms::ui
});
t->updateView(model.get());
initTooltipsForWidgetTree(*t);
}
} // namespace lms::ui
+39
View File
@@ -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
+30
View File
@@ -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
View File
@@ -27,6 +27,7 @@
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Image.hpp"
#include "database/Release.hpp"
#include "database/ScanSettings.hpp"
#include "database/Session.hpp"
@@ -62,21 +63,31 @@ namespace lms::ui::utils
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>() };
cover->setImageLink(LmsApp->getCoverResource()->getReleaseUrl(releaseId, size));
cover->setImageLink(LmsApp->getArtworkResource()->getReleaseCoverUrl(releaseId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
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>() };
cover->setImageLink(LmsApp->getCoverResource()->getTrackUrl(trackId, size));
cover->setImageLink(LmsApp->getArtworkResource()->getTrackImageUrl(trackId, size));
cover->setStyleClass("Lms-cover img-fluid"); // HACK
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
return cover;
}
@@ -212,8 +223,14 @@ namespace lms::ui::utils
matchCount += 1;
}
if (matchCount != artistIds.size())
return createArtistAnchorList(artistIds, cssAnchorClass);
if (matchCount == artistIds.size())
{
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;
}
+4 -3
View File
@@ -34,7 +34,7 @@
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "resource/CoverResource.hpp"
#include "resource/ArtworkResource.hpp"
namespace lms::db
{
@@ -54,8 +54,9 @@ namespace lms::ui::utils
{
std::string durationToString(std::chrono::milliseconds msDuration);
std::unique_ptr<Wt::WImage> createCover(db::ReleaseId releaseId, CoverResource::Size size);
std::unique_ptr<Wt::WImage> createCover(db::TrackId trackId, CoverResource::Size size);
std::unique_ptr<Wt::WImage> createReleaseCover(db::ReleaseId releaseId, ArtworkResource::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> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
+5
View File
@@ -274,6 +274,11 @@ namespace lms::ui
.arg(stepStats.progress()));
break;
case ScanStep::AssociateReleaseImages:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-release-images")
.arg(stepStats.progress()));
break;
case ScanStep::CheckForDuplicatedFiles:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
.arg(stepStats.processedElems));
+2 -2
View File
@@ -40,7 +40,7 @@ namespace lms::ui
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)
{
@@ -51,7 +51,7 @@ namespace lms::ui
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)
{
+8 -3
View File
@@ -28,9 +28,14 @@ namespace lms::ui::ArtistListHelpers
{
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")) };
res->bindWidget("name", utils::createArtistAnchor(artist));
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
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
+2 -2
View File
@@ -108,7 +108,7 @@ namespace lms::ui
if (!artistId)
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() };
@@ -197,7 +197,7 @@ namespace lms::ui
params.setClusters(_filters.getClusters());
params.setMediaLibrary(_filters.getMediaLibrary());
params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {});
params.setSortMethod(ReleaseSortMethod::OriginalDateDesc);
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
const auto releases{ Release::findIds(LmsApp->getDbSession(), params) };
if (!releases.results.empty())
+3 -3
View File
@@ -43,9 +43,9 @@ namespace lms::ui::releaseListHelpers
{
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Large) };
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
anchor->setImage(std::move(cover));
auto image{ utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large) };
image->addStyleClass("Lms-cover-release Lms-cover-anchor"); // hack
anchor->setImage(std::move(image));
}
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : ArtistId{}, "link-secondary") };
+1 -1
View File
@@ -245,7 +245,7 @@ namespace lms::ui
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") };
{
+7 -7
View File
@@ -43,7 +43,7 @@
#include "Utils.hpp"
#include "common/Template.hpp"
#include "explore/PlayQueueController.hpp"
#include "resource/CoverResource.hpp"
#include "resource/ArtworkResource.hpp"
#include "resource/DownloadResource.hpp"
namespace lms::ui::TrackListHelpers
@@ -173,20 +173,20 @@ namespace lms::ui::TrackListHelpers
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())
{
entry->setCondition("if-has-release", true);
entry->bindWidget("release", utils::createReleaseAnchor(track->getRelease()));
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
auto cover{ utils::createCover(release->getId(), CoverResource::Size::Small) };
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
anchor->setImage(std::move((cover)));
image->addStyleClass("Lms-cover-anchor"); // HACK
anchor->setImage(std::move((image)));
}
else
{
auto cover{ utils::createCover(trackId, CoverResource::Size::Small) };
cover->addStyleClass("Lms-cover-track"); // HACK
entry->bindWidget<Wt::WImage>("cover", std::move(cover));
entry->bindWidget<Wt::WImage>("cover", std::move(image));
}
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
+241
View File
@@ -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 "database/ArtistId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
namespace lms::ui
{
class CoverResource : public Wt::WResource
class ArtworkResource : public Wt::WResource
{
public:
static const std::size_t maxSize{ 512 };
CoverResource();
~CoverResource();
ArtworkResource();
~ArtworkResource();
enum class Size : std::size_t
{
@@ -40,10 +41,18 @@ namespace lms::ui
Large = 512,
};
std::string getReleaseUrl(db::ReleaseId releaseId, Size size) const;
std::string getTrackUrl(db::TrackId trackId, Size size) const;
std::string getArtistImageUrl(db::ArtistId artistId, Size size) const;
std::string getReleaseCoverUrl(db::ReleaseId releaseId, Size size) const;
std::string getTrackImageUrl(db::TrackId trackId, Size size) const;
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;
};
} // namespace lms::ui
-124
View File
@@ -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
+1 -1
View File
@@ -4,7 +4,7 @@ add_executable(lms-cover
)
target_link_libraries(lms-cover PRIVATE
lmsservice-cover
lmsartwork
Boost::program_options
)
+15 -4
View File
@@ -28,12 +28,13 @@
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/StreamLogger.hpp"
#include "core/SystemPaths.hpp"
#include "database/Db.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "image/Image.hpp"
#include "services/cover/ICoverService.hpp"
#include "services/artwork/IArtworkService.hpp"
namespace lms
{
@@ -50,7 +51,7 @@ namespace lms
for (const db::TrackId trackId : trackIds.results)
{
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
@@ -66,7 +67,17 @@ int main(int argc, char* argv[])
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout) };
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::store(po::parse_command_line(argc, argv, desc), vm);
@@ -80,7 +91,7 @@ int main(int argc, char* argv[])
image::init(argv[0]);
core::Service<core::IConfig> config{ core::createConfig(vm["conf"].as<std::string>()) };
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>()));
+2 -1
View File
@@ -32,6 +32,7 @@
#include "core/Random.hpp"
#include "core/Service.hpp"
#include "core/StreamLogger.hpp"
#include "core/SystemPaths.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -177,7 +178,7 @@ int main(int argc, char* argv[])
const GeneratorParameters defaultParams;
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::store(program_options::parse_command_line(argc, argv, options), vm);
+58 -23
View File
@@ -27,7 +27,9 @@
#include <Wt/WDate.h>
#include <boost/program_options.hpp>
#include "core/EnumSet.hpp"
#include "core/StreamLogger.hpp"
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "metadata/IParser.hpp"
@@ -241,7 +243,8 @@ int main(int argc, char* argv[])
options.add_options()
("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)")
("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
program_options::options_description hiddenOptions{ "Hidden options" };
@@ -274,13 +277,39 @@ int main(int argc, char* argv[])
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);
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& tagDelimiters{ vm["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;
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) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
parse(*parser, file);
}
catch (metadata::Exception& e)
{
std::cerr << "Parsing failed: " << e.what() << std::endl;
parse(*parser, file);
}
catch (metadata::Exception& e)
{
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) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
auto parser{ metadata::createParser(metadata::ParserBackend::TagLib, metadata::ParserReadStyle::Accurate) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
parse(*parser, file);
}
catch (metadata::Exception& e)
{
std::cerr << "Parsing failed: " << e.what() << std::endl;
parse(*parser, file);
}
catch (metadata::Exception& e)
{
std::cerr << "Parsing failed: " << e.what() << std::endl;
}
}
}
}
@@ -27,6 +27,7 @@
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/StreamLogger.hpp"
#include "core/SystemPaths.hpp"
#include "database/Artist.hpp"
#include "database/Cluster.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) };
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::store(po::parse_command_line(argc, argv, desc), vm);