Merge branch 'develop' for release v3.76.0

This commit is contained in:
emeric
2026-03-15 09:50:13 +01:00
44 changed files with 729 additions and 702 deletions
+1 -2
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12)
project(lms VERSION 3.75.0)
project(lms VERSION 3.76.0)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
@@ -21,7 +21,6 @@ endif()
find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
find_package(Filesystem REQUIRED)
find_package(Boost REQUIRED COMPONENTS program_options iostreams)
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
find_package(Pugixml CONFIG REQUIRED)
+2 -2
View File
@@ -80,7 +80,7 @@ RUN \
make -j$(nproc) install
# WT
ARG WT_VERSION=4.12.3
ARG WT_VERSION=4.12.5
ARG WT_DEBUG=OFF
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
@@ -89,7 +89,7 @@ RUN \
RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_CXX_STANDARD=20 -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DWT_CPP20_DATE_TZ_IMPLEMENTATIO="std" -DWT_CPP17_FILESYSTEM_IMPLEMENTATION="std" -DWT_CPP17_ANY_IMPLEMENTATION="std" -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON -DDEBUG=${WT_DEBUG} && \
cmake -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_CXX_STANDARD=20 -DSHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DWT_CPP20_DATE_TZ_IMPLEMENTATION="std" -DWT_CPP17_FILESYSTEM_IMPLEMENTATION="std" -DWT_CPP17_ANY_IMPLEMENTATION="std" -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON -DDEBUG=${WT_DEBUG} && \
make -j$(nproc) install
# STB
+4 -4
View File
@@ -45,7 +45,7 @@ __Optional dependencies__:
__Notes__:
* `libstb-dev` can be replaced by `libgraphicsmagick++1-dev` (the latter will likely use more RAM)
You also need _Wt4_, which is not packaged on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).</br>
You also need _Wt4_, which is not packaged on _Debian_. See [installation instructions](https://www.webtoolkit.eu/wt/doc/reference/html/InstallationUnix.html).
### Build
Get the latest stable release and build it:
```sh
@@ -99,9 +99,9 @@ systemctl restart lms
# Deployment
__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan.
## Configuration
_LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is recommended to edit this file and change relevant settings (listen address, listen port, working directory, Subsonic API activation, deployment path, ...).
All other settings are set using the web interface (user management, scan settings, transcode settings, ...).
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default configuration file](conf/lms.conf))
_LMS_ uses a configuration file installed by default at `/etc/lms.conf`. It is recommended to edit this file and adjust relevant settings such as the working directory, Wt resources path, listen address, listen port, etc.
All other settings are configured through the web interface (user management, scan settings, transcode settings, etc.).
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default configuration file](conf/lms.conf)).
## Authentication backends
You can define which authentication backend to be used thanks to the `authentication-backend` option:
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
+6 -1
View File
@@ -71,8 +71,13 @@ $setmulti(albumartists,%_albumartists%)
$setmulti(albumartistssort,%_albumartists_sort%)
```
### Extended artist and MusicBrainz ID support
_LMS_ supports several non-standard tags to allow more accurate artist identification:
* **MusicBrainz identifiers** for artist relationships: `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid`
* **Sort order variants**: , `albumartistssort`, `composerssort`, `conductorssort`, `lyricistssort`, `mixerssort`, `producerssort`, `remixerssort`. Singular forms of these tags are also accepted (e.g., `conductorsort`, `lyricistsort`, etc.)
### Album track grouping
The recommended way to group tracks within an album is to use the `MUSICBRAINZ_ALBUMID` tag.
The recommended way to group tracks within an album is to use the `musicbrainz_albumid` tag.
When this tag is not present, _LMS_ will attempt to group them as best as possible: if the analyzed file contains a disc number and the total number of discs is greater than 1, sibling directories are also scanned to find a matching album.
Otherwise, _LMS_ will only consider albums within the current directory.
+28
View File
@@ -50,4 +50,32 @@
</div>
</message>
<message id="Lms.Explore.Artist.template.appears-on-container">
<div class="mb-3">
<h3>${tr:Lms.Explore.Artist.appears-on}${artist-type class="ms-2 d-inline-block"}</h3>
${releases}
</div>
</message>
<message id="Lms.Explore.Artist.template.track-artist-type-selector">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
${selected-item}
</button>
<ul class="dropdown-menu">
<li>${artist-type-all class="dropdown-item"}</li>
<li class="dropdown-divider"></li>
${<if-has-track-artist>}<li>${artist-type-track-artist class="dropdown-item"}</li>${</if-has-track-artist>}
${<if-has-composer>}<li>${artist-type-composer class="dropdown-item"}</li>${</if-has-composer>}
${<if-has-conductor>}<li>${artist-type-conductor class="dropdown-item"}</li>${</if-has-conductor>}
${<if-has-lyricist>}<li>${artist-type-lyricist class="dropdown-item"}</li>${</if-has-lyricist>}
${<if-has-mixer>}<li>${artist-type-mixer class="dropdown-item"}</li>${</if-has-mixer>}
${<if-has-performer>}<li>${artist-type-performer class="dropdown-item"}</li>${</if-has-performer>}
${<if-has-producer>}<li>${artist-type-producer class="dropdown-item"}</li>${</if-has-producer>}
${<if-has-remixer>}<li>${artist-type-remixer class="dropdown-item"}</li>${</if-has-remixer>}
</ul>
</div>
</message>
</messages>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">Add filter</message>
<message id="Lms.Explore.all">All</message>
<message id="Lms.Explore.all-artists">All artists</message>
<message id="Lms.Explore.all-roles">All roles</message>
<message id="Lms.Explore.artists">Artists</message>
<message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.composer">
<plural case="0">Composer</plural>
<plural case="1">Composers</plural>
</message>
<message id="Lms.Explore.conductor">
<plural case="0">Conductor</plural>
<plural case="1">Conductors</plural>
</message>
<message id="Lms.Explore.copyright">Copyright</message>
<message id="Lms.Explore.cover-art">Cover art</message>
<message id="Lms.Explore.download">Download</message>
@@ -206,26 +216,54 @@
<message id="Lms.Explore.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Lyricist</plural>
<plural case="1">Lyricists</plural>
</message>
<message id="Lms.Explore.media-library">Music Library</message>
<message id="Lms.Explore.mixer">
<plural case="0">Mixer</plural>
<plural case="1">Mixers</plural>
</message>
<message id="Lms.Explore.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.performer">
<plural case="0">Performer</plural>
<plural case="1">Performers</plural>
</message>
<message id="Lms.Explore.play">Play</message>
<message id="Lms.Explore.play-last">Play last</message>
<message id="Lms.Explore.play-next">Play next</message>
<message id="Lms.Explore.play-shuffled">Play shuffled</message>
<message id="Lms.Explore.playcount">Play count</message>
<message id="Lms.Explore.producer">
<plural case="0">Producer</plural>
<plural case="1">Producers</plural>
</message>
<message id="Lms.Explore.random">Random</message>
<message id="Lms.Explore.recently-added">Recently added</message>
<message id="Lms.Explore.recently-modified">Recently modified</message>
<message id="Lms.Explore.recently-played">Recently played</message>
<message id="Lms.Explore.release-artist">
<plural case="0">Album artist</plural>
<plural case="1">Album artists</plural>
</message>
<message id="Lms.Explore.release-info">Release information</message>
<message id="Lms.Explore.release-type">Release type</message>
<message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.remixer">
<plural case="0">Remixer</plural>
<plural case="1">Remixers</plural>
</message>
<message id="Lms.Explore.search">Search</message>
<message id="Lms.Explore.star">Star</message>
<message id="Lms.Explore.star-item">Star {1}</message>
<message id="Lms.Explore.starred">Starred</message>
<message id="Lms.Explore.track-artist">
<plural case="0">Track artist</plural>
<plural case="1">Track artists</plural>
</message>
<message id="Lms.Explore.track-info">Track information</message>
<message id="Lms.Explore.track-lyrics">Lyrics</message>
<message id="Lms.Explore.tracklists">Playlists</message>
@@ -241,45 +279,6 @@
<message id="Lms.Explore.Artist.biography">Biography</message>
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">All artists</message>
<message id="Lms.Explore.Artists.composer">
<plural case="0">Composer</plural>
<plural case="1">Composers</plural>
</message>
<message id="Lms.Explore.Artists.conductor">
<plural case="0">Conductor</plural>
<plural case="1">Conductors</plural>
</message>
<message id="Lms.Explore.Artists.lyricist">
<plural case="0">Lyricist</plural>
<plural case="1">Lyricists</plural>
</message>
<message id="Lms.Explore.Artists.mixer">
<plural case="0">Mixer</plural>
<plural case="1">Mixers</plural>
</message>
<message id="Lms.Explore.Artists.performer">
<plural case="0">Performer</plural>
<plural case="1">Performers</plural>
</message>
<message id="Lms.Explore.Artists.producer">
<plural case="0">Producer</plural>
<plural case="1">Producers</plural>
</message>
<message id="Lms.Explore.Artists.release-artist">
<plural case="0">Album artist</plural>
<plural case="1">Album artists</plural>
</message>
<message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixer</plural>
<plural case="1">Remixers</plural>
</message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Track artist</plural>
<plural case="1">Track artists</plural>
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">Añadir filtro</message>
<message id="Lms.Explore.all">Todos</message>
<message id="Lms.Explore.all-artists">Todos los artistas</message>
<message id="Lms.Explore.all-roles">Todos los roles</message>
<message id="Lms.Explore.artists">Artistas</message>
<message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.composer">
<plural case="0">Compositor</plural>
<plural case="1">Compositores</plural>
</message>
<message id="Lms.Explore.conductor">
<plural case="0">Director de orquesta</plural>
<plural case="1">Directores de orquesta</plural>
</message>
<message id="Lms.Explore.copyright">Derechos de autor</message>
<message id="Lms.Explore.cover-art">Carátula</message>
<message id="Lms.Explore.download">Descargar</message>
@@ -206,26 +216,54 @@
<message id="Lms.Explore.filter-added">Filtro añadido</message>
<message id="Lms.Explore.filters">Filtros</message>
<message id="Lms.Explore.label">Sello discográfico</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Letrista</plural>
<plural case="1">Letristas</plural>
</message>
<message id="Lms.Explore.media-library">Biblioteca musical</message>
<message id="Lms.Explore.mixer">
<plural case="0">Mezclador</plural>
<plural case="1">Mezcladores</plural>
</message>
<message id="Lms.Explore.most-played">Reproducidos frecuentemente</message>
<message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Álbum de MusicBrainz</message>
<message id="Lms.Explore.performer">
<plural case="0">Intérprete</plural>
<plural case="1">Intérpretes</plural>
</message>
<message id="Lms.Explore.play">Reproducir</message>
<message id="Lms.Explore.play-last">Reproducir al final</message>
<message id="Lms.Explore.play-next">Reproducir a continuación</message>
<message id="Lms.Explore.play-shuffled">Reproducción aleatoria</message>
<message id="Lms.Explore.playcount">Número de reproducciones</message>
<message id="Lms.Explore.producer">
<plural case="0">Productor</plural>
<plural case="1">Productores</plural>
</message>
<message id="Lms.Explore.random">Aleatorio</message>
<message id="Lms.Explore.recently-added">Añadidos recientemente</message>
<message id="Lms.Explore.recently-modified">Modificados recientemente</message>
<message id="Lms.Explore.recently-played">Reproducidos recientemente</message>
<message id="Lms.Explore.release-artist">
<plural case="0">Artista del álbum</plural>
<plural case="1">Artistas del álbum</plural>
</message>
<message id="Lms.Explore.release-info">Información sobre el álbum</message>
<message id="Lms.Explore.release-type">Tipo de publicación</message>
<message id="Lms.Explore.releases">Álbumes</message>
<message id="Lms.Explore.remixer">
<plural case="0">Remezclador</plural>
<plural case="1">Remezcladores</plural>
</message>
<message id="Lms.Explore.search">Buscar</message>
<message id="Lms.Explore.star">Añadir a favoritos</message>
<message id="Lms.Explore.star-item">Agregar {1} a favoritos</message>
<message id="Lms.Explore.starred">Favoritos</message>
<message id="Lms.Explore.track-artist">
<plural case="0">Artista de la pista</plural>
<plural case="1">Artistas de la pista</plural>
</message>
<message id="Lms.Explore.track-info">Información sobre la pista</message>
<message id="Lms.Explore.track-lyrics">Letras</message>
<message id="Lms.Explore.tracklists">Listas de reproducción</message>
@@ -241,45 +279,6 @@
<message id="Lms.Explore.Artist.biography">Biografía</message>
<message id="Lms.Explore.Artist.similar-artists">Artistas similares</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">Todos los artistas</message>
<message id="Lms.Explore.Artists.composer">
<plural case="0">Compositor</plural>
<plural case="1">Compositores</plural>
</message>
<message id="Lms.Explore.Artists.conductor">
<plural case="0">Director de orquesta</plural>
<plural case="1">Directores de orquesta</plural>
</message>
<message id="Lms.Explore.Artists.lyricist">
<plural case="0">Letrista</plural>
<plural case="1">Letristas</plural>
</message>
<message id="Lms.Explore.Artists.mixer">
<plural case="0">Mezclador</plural>
<plural case="1">Mezcladores</plural>
</message>
<message id="Lms.Explore.Artists.performer">
<plural case="0">Intérprete</plural>
<plural case="1">Intérpretes</plural>
</message>
<message id="Lms.Explore.Artists.producer">
<plural case="0">Productor</plural>
<plural case="1">Productores</plural>
</message>
<message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artista del álbum</plural>
<plural case="1">Artistas del álbum</plural>
</message>
<message id="Lms.Explore.Artists.remixer">
<plural case="0">Remezclador</plural>
<plural case="1">Remezcladores</plural>
</message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artista de la pista</plural>
<plural case="1">Artistas de la pista</plural>
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
<message id="Lms.Explore.all">Tous</message>
<message id="Lms.Explore.all-artists">Tous les artistes</message>
<message id="Lms.Explore.all-roles">Tous les rôles</message>
<message id="Lms.Explore.artists">Artistes</message>
<message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.composer">
<plural case="0">Compositeur</plural>
<plural case="1">Compositeurs</plural>
</message>
<message id="Lms.Explore.conductor">
<plural case="0">Chef d'orchestre</plural>
<plural case="1">Chefs d'orchestre</plural>
</message>
<message id="Lms.Explore.copyright">Droits d'auteur</message>
<message id="Lms.Explore.cover-art">Pochette</message>
<message id="Lms.Explore.download">Télécharger</message>
@@ -206,26 +216,54 @@
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</message>
<message id="Lms.Explore.label">Label</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Parolier</plural>
<plural case="1">Paroliers</plural>
</message>
<message id="Lms.Explore.media-library">Bibliothèque musicale</message>
<message id="Lms.Explore.mixer">
<plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural>
</message>
<message id="Lms.Explore.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</message>
<message id="Lms.Explore.performer">
<plural case="0">Interprète</plural>
<plural case="1">Interprètes</plural>
</message>
<message id="Lms.Explore.play">Jouer</message>
<message id="Lms.Explore.play-last">Jouer en dernier</message>
<message id="Lms.Explore.play-next">Jouer ensuite</message>
<message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message>
<message id="Lms.Explore.playcount">Nombre d'écoutes</message>
<message id="Lms.Explore.producer">
<plural case="0">Producteur</plural>
<plural case="1">Producteurs</plural>
</message>
<message id="Lms.Explore.random">Aléatoire</message>
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
<message id="Lms.Explore.recently-played">Joués récemment</message>
<message id="Lms.Explore.release-artist">
<plural case="0">Artiste d'album</plural>
<plural case="1">Artistes d'album</plural>
</message>
<message id="Lms.Explore.release-info">Informations sur l'album</message>
<message id="Lms.Explore.release-type">Type de publication</message>
<message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.remixer">
<plural case="0">Remixeur</plural>
<plural case="1">Remixeurs</plural>
</message>
<message id="Lms.Explore.search">Rechercher</message>
<message id="Lms.Explore.star">Ajouter aux favoris</message>
<message id="Lms.Explore.star-item">Ajouter {1} aux favoris</message>
<message id="Lms.Explore.starred">Favoris</message>
<message id="Lms.Explore.track-artist">
<plural case="0">Artiste de piste</plural>
<plural case="1">Artistes de piste</plural>
</message>
<message id="Lms.Explore.track-info">Informations sur la piste</message>
<message id="Lms.Explore.track-lyrics">Paroles</message>
<message id="Lms.Explore.tracklists">Playlists</message>
@@ -241,45 +279,6 @@
<message id="Lms.Explore.Artist.biography">Biographie</message>
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">Tous les artistes</message>
<message id="Lms.Explore.Artists.composer">
<plural case="0">Compositeur</plural>
<plural case="1">Compositeurs</plural>
</message>
<message id="Lms.Explore.Artists.conductor">
<plural case="0">Chef d'orchestre</plural>
<plural case="1">Chefs d'orchestre</plural>
</message>
<message id="Lms.Explore.Artists.lyricist">
<plural case="0">Parolier</plural>
<plural case="1">Paroliers</plural>
</message>
<message id="Lms.Explore.Artists.mixer">
<plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural>
</message>
<message id="Lms.Explore.Artists.performer">
<plural case="0">Interprète</plural>
<plural case="1">Interprètes</plural>
</message>
<message id="Lms.Explore.Artists.producer">
<plural case="0">Producteur</plural>
<plural case="1">Producteurs</plural>
</message>
<message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artiste d'album</plural>
<plural case="1">Artistes d'album</plural>
</message>
<message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixeur</plural>
<plural case="1">Remixeurs</plural>
</message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artiste de piste</plural>
<plural case="1">Artistes de piste</plural>
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">Aggiungi filtro</message>
<message id="Lms.Explore.all">Tutti</message>
<message id="Lms.Explore.all-artists">Tutti gli artisti</message>
<message id="Lms.Explore.all-roles">Tutti i ruoli</message>
<message id="Lms.Explore.artists">Artisti</message>
<message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.composer">
<plural case="0">Compositore</plural>
<plural case="1">Compositori</plural>
</message>
<message id="Lms.Explore.conductor">
<plural case="0">Direttore d'orchestra</plural>
<plural case="1">Direttori d'orchestra</plural>
</message>
<message id="Lms.Explore.copyright">Diritti d'autore</message>
<message id="Lms.Explore.cover-art">Copertina</message>
<message id="Lms.Explore.download">Download</message>
@@ -206,26 +216,54 @@
<message id="Lms.Explore.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</message>
<message id="Lms.Explore.label">Etichetta</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Paroliere</plural>
<plural case="1">Parolieri</plural>
</message>
<message id="Lms.Explore.media-library">Libreria musicale</message>
<message id="Lms.Explore.mixer">
<plural case="0">Tecnico del mixaggio</plural>
<plural case="1">Tecnici del mixaggio</plural>
</message>
<message id="Lms.Explore.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Album MusicBrainz</message>
<message id="Lms.Explore.performer">
<plural case="0">Interprete</plural>
<plural case="1">Interpreti</plural>
</message>
<message id="Lms.Explore.play">Riproduci</message>
<message id="Lms.Explore.play-last">Aggiungi alla coda di riproduzione</message>
<message id="Lms.Explore.play-next">Riproduci successiva</message>
<message id="Lms.Explore.play-shuffled">Riproduci mischiando</message>
<message id="Lms.Explore.playcount">Conteggio riproduzioni</message>
<message id="Lms.Explore.producer">
<plural case="0">Produttore</plural>
<plural case="1">Produttori</plural>
</message>
<message id="Lms.Explore.random">Casuali</message>
<message id="Lms.Explore.recently-added">Aggiunti di recente</message>
<message id="Lms.Explore.recently-modified">Modificati di recente</message>
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
<message id="Lms.Explore.release-artist">
<plural case="0">Artista dell'album</plural>
<plural case="1">Artisti dell'album</plural>
</message>
<message id="Lms.Explore.release-info">Informazioni sull'album</message>
<message id="Lms.Explore.release-type">Tipo di pubblicazione</message>
<message id="Lms.Explore.releases">Album</message>
<message id="Lms.Explore.remixer">
<plural case="0">Remixer</plural>
<plural case="1">Remixers</plural>
</message>
<message id="Lms.Explore.search">Ricerca</message>
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
<message id="Lms.Explore.star-item">Aggiungi {1} ai preferiti</message>
<message id="Lms.Explore.starred">Preferiti</message>
<message id="Lms.Explore.track-artist">
<plural case="0">Artista della traccia</plural>
<plural case="1">Artisti della traccia</plural>
</message>
<message id="Lms.Explore.track-info">Informazioni sulla traccia</message>
<message id="Lms.Explore.track-lyrics">Testo</message>
<message id="Lms.Explore.tracklists">Playlist</message>
@@ -241,45 +279,6 @@
<message id="Lms.Explore.Artist.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">Tutti gli artisti</message>
<message id="Lms.Explore.Artists.composer">
<plural case="0">Compositore</plural>
<plural case="1">Compositori</plural>
</message>
<message id="Lms.Explore.Artists.conductor">
<plural case="0">Direttore d'orchestra</plural>
<plural case="1">Direttori d'orchestra</plural>
</message>
<message id="Lms.Explore.Artists.lyricist">
<plural case="0">Paroliere</plural>
<plural case="1">Parolieri</plural>
</message>
<message id="Lms.Explore.Artists.mixer">
<plural case="0">Tecnico del mixaggio</plural>
<plural case="1">Tecnici del mixaggio</plural>
</message>
<message id="Lms.Explore.Artists.performer">
<plural case="0">Interprete</plural>
<plural case="1">Interpreti</plural>
</message>
<message id="Lms.Explore.Artists.producer">
<plural case="0">Produttore</plural>
<plural case="1">Produttori</plural>
</message>
<message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artista dell'album</plural>
<plural case="1">Artisti dell'album</plural>
</message>
<message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixer</plural>
<plural case="1">Remixers</plural>
</message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artista della traccia</plural>
<plural case="1">Artisti della traccia</plural>
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message>
+47 -48
View File
@@ -213,9 +213,21 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">Dodaj filtr</message>
<message id="Lms.Explore.all">Wszystko</message>
<message id="Lms.Explore.all-artists">Wszyscy artyści</message>
<message id="Lms.Explore.all-roles">Wszystkie role</message>
<message id="Lms.Explore.artists">Artyści</message>
<message id="Lms.Explore.bitrate">Przepływność</message>
<message id="Lms.Explore.codec">Kodek</message>
<message id="Lms.Explore.composer">
<plural case="0">Kompozytor</plural>
<plural case="1">Kompozytorzy</plural>
<plural case="2">Kompozytorów</plural>
</message>
<message id="Lms.Explore.conductor">
<plural case="0">Dyrygent</plural>
<plural case="1">Dyrygenci</plural>
<plural case="2">Dyrygentów</plural>
</message>
<message id="Lms.Explore.copyright">Prawa autorskie</message>
<message id="Lms.Explore.cover-art">Okładka</message>
<message id="Lms.Explore.download">Pobierz</message>
@@ -223,26 +235,61 @@
<message id="Lms.Explore.filter-added">Filtr dodany</message>
<message id="Lms.Explore.filters">Filtry</message>
<message id="Lms.Explore.label">Wytwórnia</message>
<message id="Lms.Explore.lyricist">
<plural case="0">Tekściarz</plural>
<plural case="1">Tekściarze</plural>
<plural case="2">Tekściarzy</plural>
</message>
<message id="Lms.Explore.media-library">Biblioteka muzyczna</message>
<message id="Lms.Explore.mixer">
<plural case="0">Mikser</plural>
<plural case="1">Mikserzy</plural>
<plural case="2">Mikserów</plural>
</message>
<message id="Lms.Explore.most-played">Najczęściej odtwarzane</message>
<message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Wydanie w MusicBrainz</message>
<message id="Lms.Explore.performer">
<plural case="0">Wykonawca</plural>
<plural case="1">Wykonawcy</plural>
<plural case="2">Wykonawców</plural>
</message>
<message id="Lms.Explore.play">Odtwarzaj</message>
<message id="Lms.Explore.play-last">Odtwórz na końcu</message>
<message id="Lms.Explore.play-next">Odtwórz po obecnym</message>
<message id="Lms.Explore.play-shuffled">Odtwarzaj losowo</message>
<message id="Lms.Explore.playcount">Liczba odtworzeń</message>
<message id="Lms.Explore.producer">
<plural case="0">Producent</plural>
<plural case="1">Producenci</plural>
<plural case="2">Producentów</plural>
</message>
<message id="Lms.Explore.random">Losowo</message>
<message id="Lms.Explore.recently-added">Ostatnio dodane</message>
<message id="Lms.Explore.recently-modified">Ostatnio zmienione</message>
<message id="Lms.Explore.recently-played">Ostatnio odtwarzane</message>
<message id="Lms.Explore.release-artist">
<plural case="0">Artysta albumu</plural>
<plural case="1">Artyści albumu</plural>
<plural case="2">Artystów albumu</plural>
</message>
<message id="Lms.Explore.release-info">Informacje o wydaniu</message>
<message id="Lms.Explore.release-type">Typ wydania</message>
<message id="Lms.Explore.releases">Albumy</message>
<message id="Lms.Explore.remixer">
<plural case="0">Remikser</plural>
<plural case="1">Remikserzy</plural>
<plural case="2">Remikserów</plural>
</message>
<message id="Lms.Explore.search">Szukaj</message>
<message id="Lms.Explore.star">Wyróżnij</message>
<message id="Lms.Explore.star-item">Dodaj {1} do ulubionych</message>
<message id="Lms.Explore.starred">Wyróżnione</message>
<message id="Lms.Explore.track-artist">
<plural case="0">Artysta utworu</plural>
<plural case="1">Artyści utworu</plural>
<plural case="2">Artystów utworu</plural>
</message>
<message id="Lms.Explore.track-info">Informacje o ścieżce</message>
<message id="Lms.Explore.track-lyrics">Tekst</message>
<message id="Lms.Explore.tracklists">Listy odtwarzania</message>
@@ -258,54 +305,6 @@
<message id="Lms.Explore.Artist.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Podobni artyści</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">Wszyscy artyści</message>
<message id="Lms.Explore.Artists.composer">
<plural case="0">Kompozytor</plural>
<plural case="1">Kompozytorzy</plural>
<plural case="2">Kompozytorzy</plural>
</message>
<message id="Lms.Explore.Artists.conductor">
<plural case="0">Dyrygent</plural>
<plural case="1">Dyrygenci</plural>
<plural case="2">Dyrygenci</plural>
</message>
<message id="Lms.Explore.Artists.lyricist">
<plural case="0">Tekściarz</plural>
<plural case="1">Tekściarze</plural>
<plural case="2">Tekściarze</plural>
</message>
<message id="Lms.Explore.Artists.mixer">
<plural case="0">Realizator</plural>
<plural case="1">Realizatorzy</plural>
<plural case="2">Realizatorzy</plural>
</message>
<message id="Lms.Explore.Artists.performer">
<plural case="0">Wykonawca</plural>
<plural case="1">Wykonawcy</plural>
<plural case="2">Wykonawcy</plural>
</message>
<message id="Lms.Explore.Artists.producer">
<plural case="0">Producent</plural>
<plural case="1">Producenci</plural>
<plural case="2">Producenci</plural>
</message>
<message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artysta z albumu</plural>
<plural case="1">Artyści z albumu</plural>
<plural case="2">Artyści z albumu</plural>
</message>
<message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixer</plural>
<plural case="1">Remikserzy</plural>
<plural case="2">Remikserzy</plural>
</message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artysta ze ścieżki</plural>
<plural case="1">Artyści ze ścieżki</plural>
<plural case="2">Artyści ze ścieżki</plural>
</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
<message id="Lms.Explore.Release.disc">Dysk {1}</message>
+11 -12
View File
@@ -194,9 +194,13 @@
<!--Explore-->
<message id="Lms.Explore.add-filter">添加过滤器</message>
<message id="Lms.Explore.all">全部</message>
<message id="Lms.Explore.all-artists">所有艺术家</message>
<message id="Lms.Explore.all-roles">所有角色</message>
<message id="Lms.Explore.artists">歌手</message>
<message id="Lms.Explore.bitrate">比特率</message>
<message id="Lms.Explore.codec">编码器</message>
<message id="Lms.Explore.composer">作曲者</message>
<message id="Lms.Explore.conductor">指挥</message>
<message id="Lms.Explore.copyright">版权所有</message>
<message id="Lms.Explore.cover-art">封面</message>
<message id="Lms.Explore.download">下载</message>
@@ -204,26 +208,33 @@
<message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.label">厂牌</message>
<message id="Lms.Explore.lyricist">作词者</message>
<message id="Lms.Explore.media-library">音乐库</message>
<message id="Lms.Explore.mixer">混音师</message>
<message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
<message id="Lms.Explore.performer">表演者</message>
<message id="Lms.Explore.play">播放</message>
<message id="Lms.Explore.play-last">最后播放</message>
<message id="Lms.Explore.play-next">下一首播放</message>
<message id="Lms.Explore.play-shuffled">随机播放</message>
<message id="Lms.Explore.playcount">播放次数</message>
<message id="Lms.Explore.producer">制作人</message>
<message id="Lms.Explore.random">随机</message>
<message id="Lms.Explore.recently-added">最近添加</message>
<message id="Lms.Explore.recently-modified">最近更改</message>
<message id="Lms.Explore.recently-played">最近播放</message>
<message id="Lms.Explore.release-artist">专辑艺术家</message>
<message id="Lms.Explore.release-info">专辑信息</message>
<message id="Lms.Explore.release-type">专辑类型</message>
<message id="Lms.Explore.releases">专辑</message>
<message id="Lms.Explore.remixer">混音师</message>
<message id="Lms.Explore.search">搜索</message>
<message id="Lms.Explore.star">收藏</message>
<message id="Lms.Explore.star-item">将 {1} 加入收藏</message>
<message id="Lms.Explore.starred">已收藏</message>
<message id="Lms.Explore.track-artist">曲目艺术家</message>
<message id="Lms.Explore.track-info">曲目信息</message>
<message id="Lms.Explore.track-lyrics">歌词</message>
<message id="Lms.Explore.tracklists">播放列表</message>
@@ -239,18 +250,6 @@
<message id="Lms.Explore.Artist.biography">简介</message>
<message id="Lms.Explore.Artist.similar-artists">相似歌手</message>
<!--Explore:Artists-->
<message id="Lms.Explore.Artists.all">所有艺术家</message>
<message id="Lms.Explore.Artists.composer">作曲者</message>
<message id="Lms.Explore.Artists.conductor">指挥</message>
<message id="Lms.Explore.Artists.lyricist">作词者</message>
<message id="Lms.Explore.Artists.mixer">混音师</message>
<message id="Lms.Explore.Artists.performer">表演者</message>
<message id="Lms.Explore.Artists.producer">制作人</message>
<message id="Lms.Explore.Artists.release-artist">专辑艺术家</message>
<message id="Lms.Explore.Artists.remixer">混音师</message>
<message id="Lms.Explore.Artists.track-artist">曲目艺术家</message>
<!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">版权所有</message>
<message id="Lms.Explore.Release.disc">唱片 {1}</message>
-230
View File
@@ -1,230 +0,0 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
#[=======================================================================[.rst:
FindFilesystem
##############
This module supports the C++17 standard library's filesystem utilities. Use the
:imp-target:`std::filesystem` imported target to
Options
*******
The ``COMPONENTS`` argument to this module supports the following values:
.. find-component:: Experimental
:name: fs.Experimental
Allows the module to find the "experimental" Filesystem TS version of the
Filesystem library. This is the library that should be used with the
``std::experimental::filesystem`` namespace.
.. find-component:: Final
:name: fs.Final
Finds the final C++17 standard version of the filesystem library.
If no components are provided, behaves as if the
:find-component:`fs.Final` component was specified.
If both :find-component:`fs.Experimental` and :find-component:`fs.Final` are
provided, first looks for ``Final``, and falls back to ``Experimental`` in case
of failure. If ``Final`` is found, :imp-target:`std::filesystem` and all
:ref:`variables <fs.variables>` will refer to the ``Final`` version.
Imported Targets
****************
.. imp-target:: std::filesystem
The ``std::filesystem`` imported target is defined when any requested
version of the C++ filesystem library has been found, whether it is
*Experimental* or *Final*.
If no version of the filesystem library is available, this target will not
be defined.
.. note::
This target has ``cxx_std_17`` as an ``INTERFACE``
:ref:`compile language standard feature <req-lang-standards>`. Linking
to this target will automatically enable C++17 if no later standard
version is already required on the linking target.
.. _fs.variables:
Variables
*********
.. variable:: CXX_FILESYSTEM_IS_EXPERIMENTAL
Set to ``TRUE`` when the :find-component:`fs.Experimental` version of C++
filesystem library was found, otherwise ``FALSE``.
.. variable:: CXX_FILESYSTEM_HAVE_FS
Set to ``TRUE`` when a filesystem header was found.
.. variable:: CXX_FILESYSTEM_HEADER
Set to either ``filesystem`` or ``experimental/filesystem`` depending on
whether :find-component:`fs.Final` or :find-component:`fs.Experimental` was
found.
.. variable:: CXX_FILESYSTEM_NAMESPACE
Set to either ``std::filesystem`` or ``std::experimental::filesystem``
depending on whether :find-component:`fs.Final` or
:find-component:`fs.Experimental` was found.
Examples
********
Using `find_package(Filesystem)` with no component arguments:
.. code-block:: cmake
find_package(Filesystem REQUIRED)
add_executable(my-program main.cpp)
target_link_libraries(my-program PRIVATE std::filesystem)
#]=======================================================================]
if(TARGET std::filesystem)
# This module has already been processed. Don't do it again.
return()
endif()
include(CMakePushCheckState)
include(CheckIncludeFileCXX)
include(CheckCXXSourceCompiles)
cmake_push_check_state()
set(CMAKE_REQUIRED_QUIET ${Filesystem_FIND_QUIETLY})
# Normalize and check the component list we were given
set(want_components ${Filesystem_FIND_COMPONENTS})
if(Filesystem_FIND_COMPONENTS STREQUAL "")
set(want_components Final)
endif()
# Warn on any unrecognized components
set(extra_components ${want_components})
list(REMOVE_ITEM extra_components Final Experimental)
foreach(component IN LISTS extra_components)
message(WARNING "Extraneous find_package component for Filesystem: ${component}")
endforeach()
# Detect which of Experimental and Final we should look for
set(find_experimental TRUE)
set(find_final TRUE)
if(NOT "Final" IN_LIST want_components)
set(find_final FALSE)
endif()
if(NOT "Experimental" IN_LIST want_components)
set(find_experimental FALSE)
endif()
if(find_final)
check_include_file_cxx("filesystem" _CXX_FILESYSTEM_HAVE_HEADER)
mark_as_advanced(_CXX_FILESYSTEM_HAVE_HEADER)
if(_CXX_FILESYSTEM_HAVE_HEADER)
# We found the non-experimental header. Don't bother looking for the
# experimental one.
set(find_experimental FALSE)
endif()
else()
set(_CXX_FILESYSTEM_HAVE_HEADER FALSE)
endif()
if(find_experimental)
check_include_file_cxx("experimental/filesystem" _CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
mark_as_advanced(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
else()
set(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER FALSE)
endif()
if(_CXX_FILESYSTEM_HAVE_HEADER)
set(_have_fs TRUE)
set(_fs_header filesystem)
set(_fs_namespace std::filesystem)
elseif(_CXX_FILESYSTEM_HAVE_EXPERIMENTAL_HEADER)
set(_have_fs TRUE)
set(_fs_header experimental/filesystem)
set(_fs_namespace std::experimental::filesystem)
else()
set(_have_fs FALSE)
endif()
set(CXX_FILESYSTEM_HAVE_FS ${_have_fs} CACHE BOOL "TRUE if we have the C++ filesystem headers")
set(CXX_FILESYSTEM_HEADER ${_fs_header} CACHE STRING "The header that should be included to obtain the filesystem APIs")
set(CXX_FILESYSTEM_NAMESPACE ${_fs_namespace} CACHE STRING "The C++ namespace that contains the filesystem APIs")
set(_found FALSE)
if(CXX_FILESYSTEM_HAVE_FS)
# We have some filesystem library available. Do link checks
string(CONFIGURE [[
#include <@CXX_FILESYSTEM_HEADER@>
int main() {
auto cwd = @CXX_FILESYSTEM_NAMESPACE@::current_path();
return static_cast<int>(cwd.string().size());
}
]] code @ONLY)
# Try to compile a simple filesystem program without any linker flags
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED)
set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED})
if(NOT CXX_FILESYSTEM_NO_LINK_NEEDED)
set(prev_libraries ${CMAKE_REQUIRED_LIBRARIES})
# Try to compile a simple filesystem program with the libstdc++ flag
set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lstdc++fs)
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_STDCPPFS_NEEDED)
set(can_link ${CXX_FILESYSTEM_STDCPPFS_NEEDED})
if(NOT CXX_FILESYSTEM_STDCPPFS_NEEDED)
# Try to compile a simple filesystem program with the libc++ flag
set(CMAKE_REQUIRED_LIBRARIES ${prev_libraries} -lc++fs)
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_CPPFS_NEEDED)
set(can_link ${CXX_FILESYSTEM_CPPFS_NEEDED})
if(NOT CXX_FILESYSTEM_CPPFS_NEEDED)
# Try to compile a simple filesystem program without any linker flags
check_cxx_source_compiles("${code}" CXX_FILESYSTEM_NO_LINK_NEEDED)
set(can_link ${CXX_FILESYSTEM_NO_LINK_NEEDED})
endif()
endif()
endif()
if(can_link)
add_library(std::filesystem INTERFACE IMPORTED)
target_compile_features(std::filesystem INTERFACE cxx_std_17)
set(_found TRUE)
if(CXX_FILESYSTEM_NO_LINK_NEEDED)
# Nothing to add...
elseif(CXX_FILESYSTEM_STDCPPFS_NEEDED)
target_link_libraries(std::filesystem INTERFACE -lstdc++fs)
elseif(CXX_FILESYSTEM_CPPFS_NEEDED)
target_link_libraries(std::filesystem INTERFACE -lc++fs)
endif()
endif()
endif()
cmake_pop_check_state()
set(Filesystem_FOUND ${_found} CACHE BOOL "TRUE if we can compile and link a program using std::filesystem" FORCE)
if(Filesystem_FIND_REQUIRED AND NOT Filesystem_FOUND)
message(FATAL_ERROR "Cannot Compile simple program using std::filesystem")
endif()
-1
View File
@@ -44,7 +44,6 @@ target_include_directories(lmsaudio PRIVATE
target_link_libraries(lmsaudio PUBLIC
lmscore
std::filesystem
)
target_link_libraries(lmsaudio PRIVATE
-1
View File
@@ -54,7 +54,6 @@ target_link_libraries(lmscore PRIVATE
)
target_link_libraries(lmscore PUBLIC
std::filesystem
Wt::Wt
)
-1
View File
@@ -64,7 +64,6 @@ target_link_libraries(lmsdatabase PRIVATE
target_link_libraries(lmsdatabase PUBLIC
lmscore
std::filesystem
Wt::Dbo
)
@@ -27,7 +27,6 @@
#include <Wt/WDateTime.h>
#include "core/EnumSet.hpp"
#include "core/UUID.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
@@ -118,7 +117,7 @@ namespace lms::db
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
void setListenBrainzToken(const std::optional<core::UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
void setListenBrainzToken(std::string_view token) { _listenbrainzToken = token; }
// read
bool isAdmin() const { return _type == UserType::ADMIN; }
@@ -134,7 +133,7 @@ namespace lms::db
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
std::optional<core::UUID> getListenBrainzToken() const { return core::UUID::fromString(_listenbrainzToken); }
std::string_view getListenBrainzToken() const { return _listenbrainzToken; }
template<class Action>
void persist(Action& a)
-1
View File
@@ -14,7 +14,6 @@ target_include_directories(lmsimage PRIVATE
target_link_libraries(lmsimage PUBLIC
lmscore
std::filesystem
)
set(LMS_IMAGE_BACKEND "stb" CACHE STRING "Image library")
-1
View File
@@ -22,5 +22,4 @@ target_link_libraries(lmsartwork PUBLIC
lmsdatabase
lmsimage
lmscore
std::filesystem
)
@@ -118,13 +118,13 @@ namespace lms::feedback::listenBrainz
return;
}
const std::optional<core::UUID> listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() };
if (!listenBrainzToken)
const std::string listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() };
if (listenBrainzToken.empty())
return;
core::http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/feedback/recording-feedback";
request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() });
request.message.addHeader("Authorization", "Token " + listenBrainzToken);
Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value{ std::string{ recordingMBID->getAsString() } };
@@ -310,8 +310,8 @@ namespace lms::feedback::listenBrainz
{
assert(context.listenBrainzUserName.empty());
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (!listenBrainzToken)
const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (listenBrainzToken.empty())
{
onSyncEnded(context);
return;
@@ -320,7 +320,7 @@ namespace lms::feedback::listenBrainz
core::http::ClientGETRequestParameters request;
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token";
request.headers = { { "Authorization", "Token " + std::string{ listenBrainzToken->getAsString() } } };
request.headers = { { "Authorization", "Token " + listenBrainzToken } };
request.onSuccessFunc = [this, &context](const Wt::Http::Message& msg) {
context.listenBrainzUserName = utils::parseValidateToken(msg.body());
if (context.listenBrainzUserName.empty())
@@ -27,15 +27,18 @@
namespace lms::feedback::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId)
std::string getListenBrainzToken(db::Session& session, db::UserId userId)
{
auto transaction{ session.createReadTransaction() };
std::string res;
const db::User::pointer user{ db::User::find(session, userId) };
if (!user)
return std::nullopt;
{
auto transaction{ session.createReadTransaction() };
return user->getListenBrainzToken();
if (const db::User::pointer user{ db::User::find(session, userId) })
res = std::string{ user->getListenBrainzToken() };
}
return res;
}
std::string parseValidateToken(std::string_view msgBody)
@@ -20,7 +20,7 @@
#pragma once
#include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/objects/UserId.hpp"
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::feedback::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::string getListenBrainzToken(db::Session& session, db::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
} // namespace lms::feedback::listenBrainz::utils
@@ -21,9 +21,7 @@ target_include_directories(lmsrecommendation PRIVATE
)
target_link_libraries(lmsrecommendation PRIVATE
lmssom
std::filesystem
)
target_link_libraries(lmsrecommendation PUBLIC
-1
View File
@@ -57,7 +57,6 @@ target_link_libraries(lmsscanner PRIVATE
target_link_libraries(lmsscanner PUBLIC
lmsdatabase
std::filesystem
Wt::Wt
)
@@ -291,34 +291,32 @@ namespace lms::scrobbling::listenBrainz
return;
}
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
if (!listenBrainzToken)
const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
if (listenBrainzToken.empty())
{
LOG(DEBUG, "No listenbrainz token found: skipping");
return;
}
request.message.addBodyText(bodyText);
request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() });
request.message.addHeader("Authorization", "Token " + listenBrainzToken);
request.message.addHeader("Content-Type", "application/json");
_client.sendPOSTRequest(std::move(request));
}
bool ListensSynchronizer::saveListen(const TimedListen& listen, db::SyncState scrobblingState)
{
using namespace db;
Session& session{ _db.getTLSSession() };
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createWriteTransaction() }; // TODO: unique only if needed
db::Listen::pointer dbListen{ db::Listen::find(session, listen.userId, listen.trackId, db::ScrobblingBackend::ListenBrainz, listen.listenedAt) };
if (!dbListen)
{
const User::pointer user{ User::find(session, listen.userId) };
const db::User::pointer user{ db::User::find(session, listen.userId) };
if (!user)
return false;
const Track::pointer track{ Track::find(session, listen.trackId) };
const db::Track::pointer track{ db::Track::find(session, listen.trackId) };
if (!track)
return false;
@@ -464,8 +462,8 @@ namespace lms::scrobbling::listenBrainz
{
assert(context.listenBrainzUserName.empty());
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (!listenBrainzToken)
const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (listenBrainzToken.empty())
{
onSyncEnded(context);
return;
@@ -474,7 +472,7 @@ namespace lms::scrobbling::listenBrainz
core::http::ClientGETRequestParameters request;
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token";
request.headers = { { "Authorization", "Token " + std::string{ listenBrainzToken->getAsString() } } };
request.headers = { { "Authorization", "Token " + listenBrainzToken } };
request.onSuccessFunc = [this, &context](const Wt::Http::Message& msg) {
context.listenBrainzUserName = utils::parseValidateToken(msg.body());
if (context.listenBrainzUserName.empty())
@@ -55,7 +55,7 @@ namespace lms::scrobbling::listenBrainz
private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
bool saveListen(const TimedListen& listen, db::SyncState scrobblinState);
bool saveListen(const TimedListen& listen, db::SyncState scrobblingState);
void enquePendingListens();
@@ -27,15 +27,18 @@
namespace lms::scrobbling::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId)
std::string getListenBrainzToken(db::Session& session, db::UserId userId)
{
auto transaction{ session.createReadTransaction() };
std::string res;
const db::User::pointer user{ db::User::find(session, userId) };
if (!user)
return std::nullopt;
{
auto transaction{ session.createReadTransaction() };
return user->getListenBrainzToken();
if (const db::User::pointer user{ db::User::find(session, userId) })
res = user->getListenBrainzToken();
}
return res;
}
std::string parseValidateToken(std::string_view msgBody)
@@ -20,7 +20,7 @@
#pragma once
#include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/objects/UserId.hpp"
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[listenbrainz] " << message)
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::scrobbling::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::string getListenBrainzToken(db::Session& session, db::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
} // namespace lms::scrobbling::listenBrainz::utils
-1
View File
@@ -55,7 +55,6 @@ target_include_directories(lmssubsonic PRIVATE
)
target_link_libraries(lmssubsonic PRIVATE
std::filesystem
lmsartwork
lmsaudio
lmsauth
@@ -107,5 +107,4 @@ namespace lms::api::subsonic
{
return _isOpenSubsonicEnabled;
}
} // namespace lms::api::subsonic
@@ -77,5 +77,4 @@ namespace lms::api::subsonic
const ProtocolVersion _serverProtocolVersion;
const bool _isOpenSubsonicEnabled;
};
} // namespace lms::api::subsonic
+76 -65
View File
@@ -36,7 +36,6 @@
#include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp"
#include "ProtocolVersion.hpp"
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
#include "endpoints/AlbumSongLists.hpp"
@@ -291,91 +290,48 @@ namespace lms::api::subsonic
const std::size_t requestId{ curRequestId++ };
TLSMonotonicMemoryResourceCleaner memoryResourceCleaner;
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap()));
constexpr std::string_view optionalSuffix{ ".view" };
std::string requestPath{ request.pathInfo() };
if (core::stringUtils::stringEndsWith(requestPath, ".view"))
requestPath.resize(requestPath.length() - 5);
if (core::stringUtils::stringEndsWith(requestPath, optionalSuffix))
requestPath.resize(requestPath.length() - optionalSuffix.size());
RequestContext requestContext{ request, _db.getTLSSession(), _config };
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " to '" << requestPath << " with params = " << parameterMapToDebugString(request.getParameterMap()) << "', continuation = " << (request.continuation() ? "true" : "false"));
// First check for media retrieval endpoints
auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) };
if (itStreamHandler != mediaRetrievalHandlers.end())
{
try
{
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
handleMediaRetrievalRequest(itStreamHandler->second, requestContext, request, response);
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestId << "', code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
}
return;
}
// Now check other endpoints
try
{
if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end())
{
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
if (!handleMediaRetrievalRequest(requestPath, request, response))
handleRequest(requestPath, request, response);
db::User::pointer user;
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
{
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes);
requestContext.setUser(user);
}
const Response resp{ [&] {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
return itEntryPoint->second.func(requestContext);
}() };
{
LMS_SCOPED_TRACE_DETAILED("Subsonic", "WriteResponse");
resp.write(response.out(), requestContext.getResponseFormat());
response.setMimeType(std::string{ ResponseFormatToMimeType(requestContext.getResponseFormat()) });
}
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
return;
}
// do not disclose unhandled commands for unauthenticated users
authenticateUser(request);
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{};
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " to '" << requestPath << "' handled!");
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
Response resp{ Response::createFailedResponse(requestContext.getServerProtocolVersion(), e) };
resp.write(response.out(), requestContext.getResponseFormat());
response.setMimeType(std::string{ ResponseFormatToMimeType(requestContext.getResponseFormat()) });
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request " << requestId << " to '" << requestPath << "' with params = " << parameterMapToDebugString(request.getParameterMap()) << ": code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
}
}
void SubsonicResource::handleMediaRetrievalRequest(const MediaRetrievalHandlerFunc& handler, RequestContext& requestContext, const Wt::Http::Request& request, Wt::Http::Response& response)
bool SubsonicResource::handleMediaRetrievalRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response)
{
auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) };
if (itStreamHandler == mediaRetrievalHandlers.end())
return false;
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
try
{
// Media retrieval endpoints are always authenticated
// Optimization: no need to reauth user for each continuation
RequestContext requestContext{ request, _db.getTLSSession(), _config };
// Media retrieval endpoints are always authenticated but we don't reauth user for a continuation
db::User::pointer user;
if (!request.continuation())
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
requestContext.setUser(user);
handler(requestContext, request, response);
itStreamHandler->second(requestContext, request, response);
return true;
}
catch (const UserNotAuthorizedError&)
{
@@ -409,6 +365,61 @@ namespace lms::api::subsonic
}
}
void SubsonicResource::handleRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response)
{
auto writeResponse{ [&](const Response& resp, ResponseFormat format) {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "WriteResponse");
resp.write(response.out(), format);
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
} };
std::optional<RequestContext> requestContext;
try
{
requestContext.emplace(request, _db.getTLSSession(), _config);
}
catch (const Error& e)
{
writeResponse(Response::createFailedResponse(defaultServerProtocolVersion, e), ResponseFormat::xml);
throw;
}
try
{
if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end())
{
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
db::User::pointer user;
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
{
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes);
requestContext->setUser(user);
}
const Response resp{ [&] {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
return itEntryPoint->second.func(*requestContext);
}() };
writeResponse(resp, requestContext->getResponseFormat());
return;
}
// do not disclose unhandled commands for unauthenticated users
authenticateUser(request);
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{};
}
catch (const Error& e)
{
Response resp{ Response::createFailedResponse(requestContext->getServerProtocolVersion(), e) };
writeResponse(resp, requestContext->getResponseFormat());
throw;
}
}
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
{
const auto& parameters{ request.getParameterMap() };
+4 -2
View File
@@ -18,6 +18,8 @@
*/
#pragma once
#include <string>
#include <Wt/Http/Request.h>
#include <Wt/Http/Response.h>
#include <Wt/WResource.h>
@@ -43,8 +45,8 @@ namespace lms::api::subsonic
private:
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
void handleMediaRetrievalRequest(const MediaRetrievalHandlerFunc& handler, RequestContext& requestContext, const Wt::Http::Request& request, Wt::Http::Response& response);
bool handleMediaRetrievalRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response);
void handleRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response);
db::UserId authenticateUser(const Wt::Http::Request& request);
+100 -78
View File
@@ -24,6 +24,7 @@
#include <mutex>
#include "core/Random.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/Directory.hpp"
@@ -172,37 +173,48 @@ namespace lms::api::subsonic
if (!keywords.empty())
{
if (keywords.size() == 1)
{
if (const auto mbid{ core::UUID::fromString(keywords.front()) })
{
if (const auto artist{ Artist::find(context.getDbSession(), *mbid) })
{
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
return;
}
}
}
findArtists();
return;
}
ScanTracker<ArtistId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = artistOffset
};
if (ArtistId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Artist::find(
context.getDbSession(), cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) {
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
ScanTracker<ArtistId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = artistOffset
};
findArtists();
}
if (ArtistId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Artist::find(
context.getDbSession(), cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) {
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findArtists();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = artistOffset + artistCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = artistOffset + artistCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
}
@@ -236,37 +248,48 @@ namespace lms::api::subsonic
if (!keywords.empty())
{
if (keywords.size() == 1)
{
if (const auto mbid{ core::UUID::fromString(keywords.front()) })
{
if (const auto release{ Release::find(context.getDbSession(), *mbid) })
{
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
return;
}
}
}
findReleases();
return;
}
ScanTracker<ReleaseId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = albumOffset
};
if (ReleaseId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Release::find(
context.getDbSession(), cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) {
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
ScanTracker<ReleaseId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = albumOffset
};
findReleases();
}
if (ReleaseId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Release::find(
context.getDbSession(), cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) {
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findReleases();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = albumOffset + albumCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = albumOffset + albumCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
}
@@ -301,36 +324,35 @@ namespace lms::api::subsonic
if (!keywords.empty())
{
findTracks();
return;
}
ScanTracker<TrackId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = songOffset
};
if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Track::find(
context.getDbSession(), cachedLastRetrievedId, songCount, [&](const Track::pointer& track) {
searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
ScanTracker<TrackId>::ScanInfo scanInfo{
.clientAddress = context.getClientIpAddr(),
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = songOffset
};
findTracks();
}
if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid())
{
Track::find(
context.getDbSession(), cachedLastRetrievedId, songCount, [&](const Track::pointer& track) {
searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findTracks();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = songOffset + songCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = songOffset + songCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
}
@@ -19,8 +19,11 @@
#include "responses/DiscTitle.hpp"
#include "database/objects/Artwork.hpp"
#include "database/objects/Medium.hpp"
#include "CoverArtId.hpp"
namespace lms::api::subsonic
{
Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium)
@@ -29,6 +32,11 @@ namespace lms::api::subsonic
discTitleNode.setAttribute("disc", medium->getPosition() ? *medium->getPosition() : 0);
discTitleNode.setAttribute("title", medium->getName());
if (const auto artwork{ medium->getPreferredArtwork() })
{
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
discTitleNode.setAttribute("coverArt", idToString(coverArtId));
}
return discTitleNode;
}
+1
View File
@@ -49,6 +49,7 @@ add_executable(lms
ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp
ui/explore/ReleaseTypes.cpp
ui/explore/TrackArtistTypeSelector.cpp
ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp
ui/explore/TrackListView.cpp
+43 -3
View File
@@ -17,6 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <filesystem>
#include <thread>
#include <Wt/WApplication.h>
@@ -110,19 +111,58 @@ namespace lms
throw core::LmsException{ "Invalid config value for 'jukebox-audio-backend'" };
}
std::vector<std::string> generateWtConfig(std::string execPath)
std::error_code checkDirectoryAccessible(const std::filesystem::path& dir)
{
std::error_code ec;
const std::filesystem::file_status status{ std::filesystem::status(dir, ec) };
if (ec)
return ec;
if (status.type() != std::filesystem::file_type::directory)
return std::make_error_code(std::errc::not_a_directory);
const std::filesystem::directory_iterator it{ dir, ec };
if (ec)
return ec;
if (it != std::filesystem::directory_iterator{})
{
it->status(ec);
if (ec)
return ec;
}
return {};
}
std::vector<std::string> generateWtConfig(const std::string& execPath)
{
core::IConfig& config{ *core::Service<core::IConfig>::get() };
std::vector<std::string> args;
const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" };
const std::filesystem::path workingDirectoryPath{ config.getPath("working-dir", "/var/lms") };
const std::filesystem::path wtConfigPath{ workingDirectoryPath / "wt_config.xml" };
const std::filesystem::path wtResourcesPath{ config.getPath("wt-resources", "/usr/share/Wt/resources") };
const std::filesystem::path appRootPath{ config.getString("approot", "/usr/share/lms/approot") };
auto checkDirectoryExists{ [](const std::filesystem::path& directory, std::string_view settingName) {
std::string error;
const std::error_code ec{ checkDirectoryAccessible(directory) };
if (ec)
throw core::LmsException{ "Cannot access directory '" + directory.string() + "' specified in setting '" + std::string{ settingName } + "': " + ec.message() };
} };
checkDirectoryExists(workingDirectoryPath, "working-dir");
checkDirectoryExists(wtResourcesPath, "wt-resources");
checkDirectoryExists(appRootPath, "approot");
args.push_back(execPath);
args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + std::string{ config.getString("docroot", "/usr/share/lms/docroot/;/resources,/css,/images,/js,/favicon.ico") });
args.push_back("--approot=" + std::string{ config.getString("approot", "/usr/share/lms/approot") });
args.push_back("--approot=" + appRootPath.string());
args.push_back("--deploy-path=" + std::string{ config.getString("deploy-path", "/") });
if (!wtResourcesPath.empty())
args.push_back("--resources-dir=" + wtResourcesPath.string());
+14 -12
View File
@@ -32,6 +32,8 @@
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "database/objects/User.hpp"
@@ -144,7 +146,7 @@ namespace lms::ui
addField(ListenBrainzTokenField);
setValidator(SubsonicTokenField, createUUIDValidator());
setValidator(ListenBrainzTokenField, createUUIDValidator());
setValidator(ListenBrainzTokenField, createMandatoryValidator());
if (_authPasswordService)
{
@@ -284,14 +286,14 @@ namespace lms::ui
if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) })
user.modify()->setFeedbackBackend(_feedbackBackendModel->getValue(*feedbackBackendRow));
user.modify()->setListenBrainzToken(core::UUID::fromString(Wt::asString(value(ListenBrainzTokenField)).toUTF8()));
user.modify()->setListenBrainzToken(Wt::asString(value(ListenBrainzTokenField)).toUTF8());
}
{
if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromString(valueText(ScrobblingBackendField)) })
user.modify()->setScrobblingBackend(_scrobblingBackendModel->getValue(*scrobblingBackendRow));
user.modify()->setListenBrainzToken(core::UUID::fromString(Wt::asString(value(ListenBrainzTokenField)).toUTF8()));
user.modify()->setListenBrainzToken(Wt::asString(value(ListenBrainzTokenField)).toUTF8());
}
if (_authPasswordService && !valueText(PasswordField).empty())
@@ -390,8 +392,8 @@ namespace lms::ui
if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) })
setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow));
if (auto listenBrainzToken{ user->getListenBrainzToken() })
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() }));
if (const auto listenBrainzToken{ user->getListenBrainzToken() }; !listenBrainzToken.empty())
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken }));
{
const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz };
@@ -457,13 +459,13 @@ namespace lms::ui
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name);
_artistRelationshipsModel = std::make_shared<ArtistRelationshipsModel>();
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.composer", 2), db::TrackArtistLinkType::Composer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.conductor", 2), db::TrackArtistLinkType::Conductor);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.lyricist", 2), db::TrackArtistLinkType::Lyricist);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.mixer", 2), db::TrackArtistLinkType::Mixer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.performer", 2), db::TrackArtistLinkType::Performer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.producer", 2), db::TrackArtistLinkType::Producer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.remixer", 2), db::TrackArtistLinkType::Remixer);
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
+14 -14
View File
@@ -407,17 +407,17 @@ namespace lms::ui::utils
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer");
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.producer");
addPerformerArtists();
if (!rolelessPerformers.empty())
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) };
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.performer", rolelessPerformers.size()) };
artistMap[performersStr] = std::move(rolelessPerformers);
}
@@ -458,17 +458,17 @@ namespace lms::ui::utils
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer");
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.producer");
addPerformerArtists();
if (!rolelessPerformers.empty())
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) };
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.performer", rolelessPerformers.size()) };
artistMap[performersStr] = std::move(rolelessPerformers);
}
+2
View File
@@ -30,6 +30,8 @@
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp"
#include "database/objects/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
+10 -10
View File
@@ -30,16 +30,16 @@ namespace lms::ui
ArtistTypeSelector(ArtistType defaultArtistType)
: DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artists.template.artist-type-selector"), defaultArtistType }
{
bindItem("artist-type-all", Wt::WString::tr("Lms.Explore.Artists.all"), AllArtistsTag{});
bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.Artists.release-artist", 2), ReleaseArtistsTag{});
bindItem("artist-type-track-artist", Wt::WString::trn("Lms.Explore.Artists.track-artist", 2), db::TrackArtistLinkType::Artist);
bindItem("artist-type-composer", Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer);
bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor);
bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist);
bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer);
bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer);
bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer);
bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer);
bindItem("artist-type-all", Wt::WString::tr("Lms.Explore.all-artists"), AllArtistsTag{});
bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.release-artist", 2), ReleaseArtistsTag{});
bindItem("artist-type-track-artist", Wt::WString::trn("Lms.Explore.track-artist", 2), db::TrackArtistLinkType::Artist);
bindItem("artist-type-composer", Wt::WString::trn("Lms.Explore.composer", 2), db::TrackArtistLinkType::Composer);
bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.conductor", 2), db::TrackArtistLinkType::Conductor);
bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.lyricist", 2), db::TrackArtistLinkType::Lyricist);
bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.mixer", 2), db::TrackArtistLinkType::Mixer);
bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.performer", 2), db::TrackArtistLinkType::Performer);
bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.producer", 2), db::TrackArtistLinkType::Producer);
bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.remixer", 2), db::TrackArtistLinkType::Remixer);
}
};
} // namespace lms::ui
+67 -14
View File
@@ -19,7 +19,10 @@
#include "ArtistView.hpp"
#include <array>
#include <Wt/WPushButton.h>
#include <Wt/WStackedWidget.h>
#include "core/String.hpp"
#include "database/Session.hpp"
@@ -30,6 +33,8 @@
#include "database/objects/Release.hpp"
#include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IRecommendationService.hpp"
@@ -40,6 +45,7 @@
#include "LmsApplicationException.hpp"
#include "PlayQueueController.hpp"
#include "ReleaseHelpers.hpp"
#include "TrackArtistTypeSelector.hpp"
#include "TrackListHelpers.hpp"
#include "Utils.hpp"
#include "common/InfiniteScrollingContainer.hpp"
@@ -272,30 +278,77 @@ namespace lms::ui
void Artist::refreshAppearsOnReleases()
{
_appearsOnReleases.clear();
_appearsOnReleaseContainer = {};
db::Release::FindParameters params;
params.setTrackArtist(_artistId);
params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
auto addToReleaseContainer = [&](std::optional<db::TrackArtistLinkType> linkType) -> bool {
db::Release::FindParameters params;
db::Release::find(LmsApp->getDbSession(), params, [this](const db::Release::pointer& release) {
if (!release->hasArtist(_artistId))
_appearsOnReleaseContainer.releases.push_back(release->getId());
});
core::EnumSet<db::TrackArtistLinkType> linkTypes;
if (linkType)
linkTypes.insert(*linkType);
if (!_appearsOnReleaseContainer.releases.empty())
params.setTrackArtist(_artistId, linkTypes);
params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
bool found{};
db::Release::find(LmsApp->getDbSession(), params, [&](const db::Release::pointer& release) {
if (release->hasArtist(_artistId))
return;
_appearsOnReleases[linkType].push_back(release->getId());
found = true;
});
return found;
};
constexpr std::array<std::optional<db::TrackArtistLinkType>, 9> types{
std::nullopt,
db::TrackArtistLinkType::Artist,
db::TrackArtistLinkType::Composer,
db::TrackArtistLinkType::Conductor,
db::TrackArtistLinkType::Lyricist,
db::TrackArtistLinkType::Mixer,
db::TrackArtistLinkType::Performer,
db::TrackArtistLinkType::Producer,
db::TrackArtistLinkType::Remixer,
};
core::EnumSet<db::TrackArtistLinkType> foundTypes;
for (const auto type : types)
{
Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) };
releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.Artist.appears-on"));
_appearsOnReleaseContainer.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
_appearsOnReleaseContainer.container->onRequestElements.connect(this, [this] {
if (addToReleaseContainer(type) && type)
foundTypes.insert(*type);
}
if (!_appearsOnReleases.empty())
{
std::optional<db::TrackArtistLinkType> defaultType{ std::nullopt };
if (_appearsOnReleases.size() == 2)
{
auto it{ std::find_if(std::cbegin(_appearsOnReleases), std::cend(_appearsOnReleases), [=](const auto& entry) { return entry.first != defaultType; }) };
if (it != std::cend(_appearsOnReleases))
defaultType = it->first;
}
Wt::WTemplate* appearsOnContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.appears-on-container")) };
appearsOnContainer->addFunction("tr", &Wt::WTemplate::Functions::tr);
_appearsOnReleaseContainer.container = appearsOnContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
_appearsOnReleaseContainer.releases = _appearsOnReleases[defaultType];
_appearsOnReleaseContainer.container->onRequestElements.connect(this, [&] {
addSomeReleases(_appearsOnReleaseContainer, { releaseListHelpers::DisplayOptions::ShowYearAndOriginalYear, releaseListHelpers::DisplayOptions::ShowArtist });
});
TrackArtistTypeSelector* trackArtistTypeSelector{ appearsOnContainer->bindNew<TrackArtistTypeSelector>("artist-type", defaultType, foundTypes) };
trackArtistTypeSelector->itemSelected.connect([this](std::optional<db::TrackArtistLinkType> newArtistLinkType) {
_appearsOnReleaseContainer.releases = _appearsOnReleases[newArtistLinkType];
_appearsOnReleaseContainer.container->reset();
});
}
else
{
bindEmpty("appears-on-releases");
}
}
void Artist::refreshNonReleaseTracks()
+4 -1
View File
@@ -20,6 +20,7 @@
#pragma once
#include <map>
#include <optional>
#include "core/EnumSet.hpp"
@@ -31,6 +32,7 @@
#include "ReleaseHelpers.hpp"
#include "ReleaseTypes.hpp"
#include "common/Template.hpp"
#include "database/objects/Types.hpp"
namespace lms::db
{
@@ -76,7 +78,8 @@ namespace lms::ui
std::vector<db::ReleaseId> releases;
};
std::map<ReleaseType, ReleaseContainer> _releaseContainers;
ReleaseContainer _appearsOnReleaseContainer{};
std::map<std::optional<db::TrackArtistLinkType>, std::vector<db::ReleaseId>> _appearsOnReleases;
ReleaseContainer _appearsOnReleaseContainer;
InfiniteScrollingContainer* _trackContainer{};
db::ArtistId _artistId{};
std::string _artistName;
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TrackArtistTypeSelector.hpp"
#include <array>
namespace lms::ui
{
TrackArtistTypeSelector::TrackArtistTypeSelector(std::optional<db::TrackArtistLinkType> defaultRole, core::EnumSet<db::TrackArtistLinkType> types)
: DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artist.template.track-artist-type-selector"), defaultRole }
{
struct Entry
{
std::string var;
Wt::WString title;
std::optional<db::TrackArtistLinkType> linkType;
core::LiteralString condition;
};
auto bindItemWithCondition = [&](const std::string& var, const Wt::WString& title, std::optional<db::TrackArtistLinkType> item, core::LiteralString condition) {
if (!condition.empty())
setCondition(condition.c_str(), true);
bindItem(var, title, item);
};
const std::array entries{
Entry{ "artist-type-all", Wt::WString::tr("Lms.Explore.all-roles"), std::nullopt, core::LiteralString{} },
Entry{ "artist-type-track-artist", Wt::WString::trn("Lms.Explore.track-artist", 1), db::TrackArtistLinkType::Artist, "if-has-track-artist" },
Entry{ "artist-type-composer", Wt::WString::trn("Lms.Explore.composer", 1), db::TrackArtistLinkType::Composer, "if-has-composer" },
Entry{ "artist-type-conductor", Wt::WString::trn("Lms.Explore.conductor", 1), db::TrackArtistLinkType::Conductor, "if-has-conductor" },
Entry{ "artist-type-lyricist", Wt::WString::trn("Lms.Explore.lyricist", 1), db::TrackArtistLinkType::Lyricist, "if-has-lyricist" },
Entry{ "artist-type-mixer", Wt::WString::trn("Lms.Explore.mixer", 1), db::TrackArtistLinkType::Mixer, "if-has-mixer" },
Entry{ "artist-type-performer", Wt::WString::trn("Lms.Explore.performer", 1), db::TrackArtistLinkType::Performer, "if-has-performer" },
Entry{ "artist-type-producer", Wt::WString::trn("Lms.Explore.producer", 1), db::TrackArtistLinkType::Producer, "if-has-producer" },
Entry{ "artist-type-remixer", Wt::WString::trn("Lms.Explore.remixer", 1), db::TrackArtistLinkType::Remixer, "if-has-remixer" },
};
for (const Entry& entry : entries)
{
if (!entry.linkType || types.contains(*entry.linkType))
bindItemWithCondition(entry.var, entry.title, entry.linkType, entry.condition);
}
}
} // namespace lms::ui
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "core/EnumSet.hpp"
#include "database/objects/Types.hpp"
#include "DropDownMenuSelector.hpp"
namespace lms::ui
{
class TrackArtistTypeSelector : public DropDownMenuSelector<std::optional<db::TrackArtistLinkType>>
{
public:
TrackArtistTypeSelector(std::optional<db::TrackArtistLinkType> defaultRole, core::EnumSet<db::TrackArtistLinkType> types);
};
} // namespace lms::ui