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) 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/) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
@@ -21,7 +21,6 @@ endif()
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
find_package(Filesystem REQUIRED)
find_package(Boost REQUIRED COMPONENTS program_options iostreams) find_package(Boost REQUIRED COMPONENTS program_options iostreams)
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
find_package(Pugixml CONFIG REQUIRED) find_package(Pugixml CONFIG REQUIRED)
+2 -2
View File
@@ -80,7 +80,7 @@ RUN \
make -j$(nproc) install make -j$(nproc) install
# WT # WT
ARG WT_VERSION=4.12.3 ARG WT_VERSION=4.12.5
ARG WT_DEBUG=OFF ARG WT_DEBUG=OFF
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
@@ -89,7 +89,7 @@ RUN \
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ 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 make -j$(nproc) install
# STB # STB
+4 -4
View File
@@ -45,7 +45,7 @@ __Optional dependencies__:
__Notes__: __Notes__:
* `libstb-dev` can be replaced by `libgraphicsmagick++1-dev` (the latter will likely use more RAM) * `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 ### Build
Get the latest stable release and build it: Get the latest stable release and build it:
```sh ```sh
@@ -99,9 +99,9 @@ systemctl restart lms
# Deployment # Deployment
__Note__: don't forget to give the _lms_ user read access to the music directory you want to scan. __Note__: don't forget to give the _lms_ user read access to the music directory you want to scan.
## Configuration ## 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, ...). _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 set using the web interface (user management, scan settings, transcode settings, ...). 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)) 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 ## Authentication backends
You can define which authentication backend to be used thanks to the `authentication-backend` option: 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. * `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%) $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 ### 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. 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. Otherwise, _LMS_ will only consider albums within the current directory.
+28
View File
@@ -50,4 +50,32 @@
</div> </div>
</message> </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> </messages>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">Add filter</message> <message id="Lms.Explore.add-filter">Add filter</message>
<message id="Lms.Explore.all">All</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.artists">Artists</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</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.copyright">Copyright</message>
<message id="Lms.Explore.cover-art">Cover art</message> <message id="Lms.Explore.cover-art">Cover art</message>
<message id="Lms.Explore.download">Download</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.filter-added">Filter added</message>
<message id="Lms.Explore.filters">Filters</message> <message id="Lms.Explore.filters">Filters</message>
<message id="Lms.Explore.label">Label</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.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.most-played">Most played</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</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">Play</message>
<message id="Lms.Explore.play-last">Play last</message> <message id="Lms.Explore.play-last">Play last</message>
<message id="Lms.Explore.play-next">Play next</message> <message id="Lms.Explore.play-next">Play next</message>
<message id="Lms.Explore.play-shuffled">Play shuffled</message> <message id="Lms.Explore.play-shuffled">Play shuffled</message>
<message id="Lms.Explore.playcount">Play count</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.random">Random</message>
<message id="Lms.Explore.recently-added">Recently added</message> <message id="Lms.Explore.recently-added">Recently added</message>
<message id="Lms.Explore.recently-modified">Recently modified</message> <message id="Lms.Explore.recently-modified">Recently modified</message>
<message id="Lms.Explore.recently-played">Recently played</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-info">Release information</message>
<message id="Lms.Explore.release-type">Release type</message> <message id="Lms.Explore.release-type">Release type</message>
<message id="Lms.Explore.releases">Albums</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.search">Search</message>
<message id="Lms.Explore.star">Star</message> <message id="Lms.Explore.star">Star</message>
<message id="Lms.Explore.star-item">Star {1}</message> <message id="Lms.Explore.star-item">Star {1}</message>
<message id="Lms.Explore.starred">Starred</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-info">Track information</message>
<message id="Lms.Explore.track-lyrics">Lyrics</message> <message id="Lms.Explore.track-lyrics">Lyrics</message>
<message id="Lms.Explore.tracklists">Playlists</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.biography">Biography</message>
<message id="Lms.Explore.Artist.similar-artists">Similar artists</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disc {1}</message> <message id="Lms.Explore.Release.disc">Disc {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">Añadir filtro</message> <message id="Lms.Explore.add-filter">Añadir filtro</message>
<message id="Lms.Explore.all">Todos</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.artists">Artistas</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</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.copyright">Derechos de autor</message>
<message id="Lms.Explore.cover-art">Carátula</message> <message id="Lms.Explore.cover-art">Carátula</message>
<message id="Lms.Explore.download">Descargar</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.filter-added">Filtro añadido</message>
<message id="Lms.Explore.filters">Filtros</message> <message id="Lms.Explore.filters">Filtros</message>
<message id="Lms.Explore.label">Sello discográfico</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.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.most-played">Reproducidos frecuentemente</message>
<message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</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.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">Reproducir</message>
<message id="Lms.Explore.play-last">Reproducir al final</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-next">Reproducir a continuación</message>
<message id="Lms.Explore.play-shuffled">Reproducción aleatoria</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.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.random">Aleatorio</message>
<message id="Lms.Explore.recently-added">Añadidos recientemente</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-modified">Modificados recientemente</message>
<message id="Lms.Explore.recently-played">Reproducidos 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-info">Información sobre el álbum</message>
<message id="Lms.Explore.release-type">Tipo de publicación</message> <message id="Lms.Explore.release-type">Tipo de publicación</message>
<message id="Lms.Explore.releases">Álbumes</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.search">Buscar</message>
<message id="Lms.Explore.star">Añadir a favoritos</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.star-item">Agregar {1} a favoritos</message>
<message id="Lms.Explore.starred">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-info">Información sobre la pista</message>
<message id="Lms.Explore.track-lyrics">Letras</message> <message id="Lms.Explore.track-lyrics">Letras</message>
<message id="Lms.Explore.tracklists">Listas de reproducción</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.biography">Biografía</message>
<message id="Lms.Explore.Artist.similar-artists">Artistas similares</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message> <message id="Lms.Explore.Release.disc">Disco {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">Ajouter filtre</message> <message id="Lms.Explore.add-filter">Ajouter filtre</message>
<message id="Lms.Explore.all">Tous</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.artists">Artistes</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</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.copyright">Droits d'auteur</message>
<message id="Lms.Explore.cover-art">Pochette</message> <message id="Lms.Explore.cover-art">Pochette</message>
<message id="Lms.Explore.download">Télécharger</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.filter-added">Filtre ajouté</message>
<message id="Lms.Explore.filters">Filtres</message> <message id="Lms.Explore.filters">Filtres</message>
<message id="Lms.Explore.label">Label</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.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.most-played">Plus joués</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz Artist</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz Release</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">Jouer</message>
<message id="Lms.Explore.play-last">Jouer en dernier</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-next">Jouer ensuite</message>
<message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message> <message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message>
<message id="Lms.Explore.playcount">Nombre d'écoutes</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.random">Aléatoire</message>
<message id="Lms.Explore.recently-added">Ajoutés récemment</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-modified">Modifiés récemment</message>
<message id="Lms.Explore.recently-played">Joué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-info">Informations sur l'album</message>
<message id="Lms.Explore.release-type">Type de publication</message> <message id="Lms.Explore.release-type">Type de publication</message>
<message id="Lms.Explore.releases">Albums</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.search">Rechercher</message>
<message id="Lms.Explore.star">Ajouter aux favoris</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.star-item">Ajouter {1} aux favoris</message>
<message id="Lms.Explore.starred">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-info">Informations sur la piste</message>
<message id="Lms.Explore.track-lyrics">Paroles</message> <message id="Lms.Explore.track-lyrics">Paroles</message>
<message id="Lms.Explore.tracklists">Playlists</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.biography">Biographie</message>
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disque {1}</message> <message id="Lms.Explore.Release.disc">Disque {1}</message>
+38 -39
View File
@@ -196,9 +196,19 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">Aggiungi filtro</message> <message id="Lms.Explore.add-filter">Aggiungi filtro</message>
<message id="Lms.Explore.all">Tutti</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.artists">Artisti</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</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.copyright">Diritti d'autore</message>
<message id="Lms.Explore.cover-art">Copertina</message> <message id="Lms.Explore.cover-art">Copertina</message>
<message id="Lms.Explore.download">Download</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.filter-added">Filtro aggiunto</message>
<message id="Lms.Explore.filters">Filtri</message> <message id="Lms.Explore.filters">Filtri</message>
<message id="Lms.Explore.label">Etichetta</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.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.most-played">Più ascoltati</message>
<message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message> <message id="Lms.Explore.musicbrainz-artist">Artista MusicBrainz</message>
<message id="Lms.Explore.musicbrainz-release">Album 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">Riproduci</message>
<message id="Lms.Explore.play-last">Aggiungi alla coda di riproduzione</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-next">Riproduci successiva</message>
<message id="Lms.Explore.play-shuffled">Riproduci mischiando</message> <message id="Lms.Explore.play-shuffled">Riproduci mischiando</message>
<message id="Lms.Explore.playcount">Conteggio riproduzioni</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.random">Casuali</message>
<message id="Lms.Explore.recently-added">Aggiunti di recente</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-modified">Modificati di recente</message>
<message id="Lms.Explore.recently-played">Riprodotti 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-info">Informazioni sull'album</message>
<message id="Lms.Explore.release-type">Tipo di pubblicazione</message> <message id="Lms.Explore.release-type">Tipo di pubblicazione</message>
<message id="Lms.Explore.releases">Album</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.search">Ricerca</message>
<message id="Lms.Explore.star">Aggiungi ai preferiti</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.star-item">Aggiungi {1} ai preferiti</message>
<message id="Lms.Explore.starred">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-info">Informazioni sulla traccia</message>
<message id="Lms.Explore.track-lyrics">Testo</message> <message id="Lms.Explore.track-lyrics">Testo</message>
<message id="Lms.Explore.tracklists">Playlist</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.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
<message id="Lms.Explore.Release.disc">Disco {1}</message> <message id="Lms.Explore.Release.disc">Disco {1}</message>
+47 -48
View File
@@ -213,9 +213,21 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">Dodaj filtr</message> <message id="Lms.Explore.add-filter">Dodaj filtr</message>
<message id="Lms.Explore.all">Wszystko</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.artists">Artyści</message>
<message id="Lms.Explore.bitrate">Przepływność</message> <message id="Lms.Explore.bitrate">Przepływność</message>
<message id="Lms.Explore.codec">Kodek</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.copyright">Prawa autorskie</message>
<message id="Lms.Explore.cover-art">Okładka</message> <message id="Lms.Explore.cover-art">Okładka</message>
<message id="Lms.Explore.download">Pobierz</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.filter-added">Filtr dodany</message>
<message id="Lms.Explore.filters">Filtry</message> <message id="Lms.Explore.filters">Filtry</message>
<message id="Lms.Explore.label">Wytwórnia</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.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.most-played">Najczęściej odtwarzane</message>
<message id="Lms.Explore.musicbrainz-artist">Artysta w MusicBrainz</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.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">Odtwarzaj</message>
<message id="Lms.Explore.play-last">Odtwórz na końcu</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-next">Odtwórz po obecnym</message>
<message id="Lms.Explore.play-shuffled">Odtwarzaj losowo</message> <message id="Lms.Explore.play-shuffled">Odtwarzaj losowo</message>
<message id="Lms.Explore.playcount">Liczba odtworzeń</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.random">Losowo</message>
<message id="Lms.Explore.recently-added">Ostatnio dodane</message> <message id="Lms.Explore.recently-added">Ostatnio dodane</message>
<message id="Lms.Explore.recently-modified">Ostatnio zmienione</message> <message id="Lms.Explore.recently-modified">Ostatnio zmienione</message>
<message id="Lms.Explore.recently-played">Ostatnio odtwarzane</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-info">Informacje o wydaniu</message>
<message id="Lms.Explore.release-type">Typ wydania</message> <message id="Lms.Explore.release-type">Typ wydania</message>
<message id="Lms.Explore.releases">Albumy</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.search">Szukaj</message>
<message id="Lms.Explore.star">Wyróżnij</message> <message id="Lms.Explore.star">Wyróżnij</message>
<message id="Lms.Explore.star-item">Dodaj {1} do ulubionych</message> <message id="Lms.Explore.star-item">Dodaj {1} do ulubionych</message>
<message id="Lms.Explore.starred">Wyróżnione</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-info">Informacje o ścieżce</message>
<message id="Lms.Explore.track-lyrics">Tekst</message> <message id="Lms.Explore.track-lyrics">Tekst</message>
<message id="Lms.Explore.tracklists">Listy odtwarzania</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.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Podobni artyści</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message> <message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
<message id="Lms.Explore.Release.disc">Dysk {1}</message> <message id="Lms.Explore.Release.disc">Dysk {1}</message>
+11 -12
View File
@@ -194,9 +194,13 @@
<!--Explore--> <!--Explore-->
<message id="Lms.Explore.add-filter">添加过滤器</message> <message id="Lms.Explore.add-filter">添加过滤器</message>
<message id="Lms.Explore.all">全部</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.artists">歌手</message>
<message id="Lms.Explore.bitrate">比特率</message> <message id="Lms.Explore.bitrate">比特率</message>
<message id="Lms.Explore.codec">编码器</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.copyright">版权所有</message>
<message id="Lms.Explore.cover-art">封面</message> <message id="Lms.Explore.cover-art">封面</message>
<message id="Lms.Explore.download">下载</message> <message id="Lms.Explore.download">下载</message>
@@ -204,26 +208,33 @@
<message id="Lms.Explore.filter-added">过滤器已添加</message> <message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message> <message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.label">厂牌</message> <message id="Lms.Explore.label">厂牌</message>
<message id="Lms.Explore.lyricist">作词者</message>
<message id="Lms.Explore.media-library">音乐库</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.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">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">播放</message>
<message id="Lms.Explore.play-last">最后播放</message> <message id="Lms.Explore.play-last">最后播放</message>
<message id="Lms.Explore.play-next">下一首播放</message> <message id="Lms.Explore.play-next">下一首播放</message>
<message id="Lms.Explore.play-shuffled">随机播放</message> <message id="Lms.Explore.play-shuffled">随机播放</message>
<message id="Lms.Explore.playcount">播放次数</message> <message id="Lms.Explore.playcount">播放次数</message>
<message id="Lms.Explore.producer">制作人</message>
<message id="Lms.Explore.random">随机</message> <message id="Lms.Explore.random">随机</message>
<message id="Lms.Explore.recently-added">最近添加</message> <message id="Lms.Explore.recently-added">最近添加</message>
<message id="Lms.Explore.recently-modified">最近更改</message> <message id="Lms.Explore.recently-modified">最近更改</message>
<message id="Lms.Explore.recently-played">最近播放</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-info">专辑信息</message>
<message id="Lms.Explore.release-type">专辑类型</message> <message id="Lms.Explore.release-type">专辑类型</message>
<message id="Lms.Explore.releases">专辑</message> <message id="Lms.Explore.releases">专辑</message>
<message id="Lms.Explore.remixer">混音师</message>
<message id="Lms.Explore.search">搜索</message> <message id="Lms.Explore.search">搜索</message>
<message id="Lms.Explore.star">收藏</message> <message id="Lms.Explore.star">收藏</message>
<message id="Lms.Explore.star-item">将 {1} 加入收藏</message> <message id="Lms.Explore.star-item">将 {1} 加入收藏</message>
<message id="Lms.Explore.starred">已收藏</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-info">曲目信息</message>
<message id="Lms.Explore.track-lyrics">歌词</message> <message id="Lms.Explore.track-lyrics">歌词</message>
<message id="Lms.Explore.tracklists">播放列表</message> <message id="Lms.Explore.tracklists">播放列表</message>
@@ -239,18 +250,6 @@
<message id="Lms.Explore.Artist.biography">简介</message> <message id="Lms.Explore.Artist.biography">简介</message>
<message id="Lms.Explore.Artist.similar-artists">相似歌手</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--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">版权所有</message> <message id="Lms.Explore.Release.copyright">版权所有</message>
<message id="Lms.Explore.Release.disc">唱片 {1}</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 target_link_libraries(lmsaudio PUBLIC
lmscore lmscore
std::filesystem
) )
target_link_libraries(lmsaudio PRIVATE target_link_libraries(lmsaudio PRIVATE
-1
View File
@@ -54,7 +54,6 @@ target_link_libraries(lmscore PRIVATE
) )
target_link_libraries(lmscore PUBLIC target_link_libraries(lmscore PUBLIC
std::filesystem
Wt::Wt Wt::Wt
) )
-1
View File
@@ -64,7 +64,6 @@ target_link_libraries(lmsdatabase PRIVATE
target_link_libraries(lmsdatabase PUBLIC target_link_libraries(lmsdatabase PUBLIC
lmscore lmscore
std::filesystem
Wt::Dbo Wt::Dbo
) )
@@ -27,7 +27,6 @@
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
#include "core/UUID.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
@@ -118,7 +117,7 @@ namespace lms::db
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; } void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; } 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 // read
bool isAdmin() const { return _type == UserType::ADMIN; } bool isAdmin() const { return _type == UserType::ADMIN; }
@@ -134,7 +133,7 @@ namespace lms::db
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; } FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; } ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
std::optional<core::UUID> getListenBrainzToken() const { return core::UUID::fromString(_listenbrainzToken); } std::string_view getListenBrainzToken() const { return _listenbrainzToken; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
-1
View File
@@ -14,7 +14,6 @@ target_include_directories(lmsimage PRIVATE
target_link_libraries(lmsimage PUBLIC target_link_libraries(lmsimage PUBLIC
lmscore lmscore
std::filesystem
) )
set(LMS_IMAGE_BACKEND "stb" CACHE STRING "Image library") set(LMS_IMAGE_BACKEND "stb" CACHE STRING "Image library")
-1
View File
@@ -22,5 +22,4 @@ target_link_libraries(lmsartwork PUBLIC
lmsdatabase lmsdatabase
lmsimage lmsimage
lmscore lmscore
std::filesystem
) )
@@ -118,13 +118,13 @@ namespace lms::feedback::listenBrainz
return; return;
} }
const std::optional<core::UUID> listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() }; const std::string listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() };
if (!listenBrainzToken) if (listenBrainzToken.empty())
return; return;
core::http::ClientPOSTRequestParameters request; core::http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/feedback/recording-feedback"; 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; Wt::Json::Object root;
root["recording_mbid"] = Wt::Json::Value{ std::string{ recordingMBID->getAsString() } }; root["recording_mbid"] = Wt::Json::Value{ std::string{ recordingMBID->getAsString() } };
@@ -310,8 +310,8 @@ namespace lms::feedback::listenBrainz
{ {
assert(context.listenBrainzUserName.empty()); assert(context.listenBrainzUserName.empty());
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) }; const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (!listenBrainzToken) if (listenBrainzToken.empty())
{ {
onSyncEnded(context); onSyncEnded(context);
return; return;
@@ -320,7 +320,7 @@ namespace lms::feedback::listenBrainz
core::http::ClientGETRequestParameters request; core::http::ClientGETRequestParameters request;
request.priority = core::http::ClientRequestParameters::Priority::Low; request.priority = core::http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token"; 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) { request.onSuccessFunc = [this, &context](const Wt::Http::Message& msg) {
context.listenBrainzUserName = utils::parseValidateToken(msg.body()); context.listenBrainzUserName = utils::parseValidateToken(msg.body());
if (context.listenBrainzUserName.empty()) if (context.listenBrainzUserName.empty())
@@ -27,15 +27,18 @@
namespace lms::feedback::listenBrainz::utils 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) auto transaction{ session.createReadTransaction() };
return std::nullopt;
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) std::string parseValidateToken(std::string_view msgBody)
@@ -20,7 +20,7 @@
#pragma once #pragma once
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/objects/UserId.hpp" #include "database/objects/UserId.hpp"
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message) #define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::feedback::listenBrainz::utils 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); std::string parseValidateToken(std::string_view msgBody);
} // namespace lms::feedback::listenBrainz::utils } // namespace lms::feedback::listenBrainz::utils
@@ -21,9 +21,7 @@ target_include_directories(lmsrecommendation PRIVATE
) )
target_link_libraries(lmsrecommendation PRIVATE target_link_libraries(lmsrecommendation PRIVATE
lmssom lmssom
std::filesystem
) )
target_link_libraries(lmsrecommendation PUBLIC target_link_libraries(lmsrecommendation PUBLIC
-1
View File
@@ -57,7 +57,6 @@ target_link_libraries(lmsscanner PRIVATE
target_link_libraries(lmsscanner PUBLIC target_link_libraries(lmsscanner PUBLIC
lmsdatabase lmsdatabase
std::filesystem
Wt::Wt Wt::Wt
) )
@@ -291,34 +291,32 @@ namespace lms::scrobbling::listenBrainz
return; return;
} }
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) }; const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), listen.userId) };
if (!listenBrainzToken) if (listenBrainzToken.empty())
{ {
LOG(DEBUG, "No listenbrainz token found: skipping"); LOG(DEBUG, "No listenbrainz token found: skipping");
return; return;
} }
request.message.addBodyText(bodyText); 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"); request.message.addHeader("Content-Type", "application/json");
_client.sendPOSTRequest(std::move(request)); _client.sendPOSTRequest(std::move(request));
} }
bool ListensSynchronizer::saveListen(const TimedListen& listen, db::SyncState scrobblingState) bool ListensSynchronizer::saveListen(const TimedListen& listen, db::SyncState scrobblingState)
{ {
using namespace db; db::Session& session{ _db.getTLSSession() };
Session& session{ _db.getTLSSession() };
auto transaction{ session.createWriteTransaction() }; // TODO: unique only if needed 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) }; db::Listen::pointer dbListen{ db::Listen::find(session, listen.userId, listen.trackId, db::ScrobblingBackend::ListenBrainz, listen.listenedAt) };
if (!dbListen) if (!dbListen)
{ {
const User::pointer user{ User::find(session, listen.userId) }; const db::User::pointer user{ db::User::find(session, listen.userId) };
if (!user) if (!user)
return false; return false;
const Track::pointer track{ Track::find(session, listen.trackId) }; const db::Track::pointer track{ db::Track::find(session, listen.trackId) };
if (!track) if (!track)
return false; return false;
@@ -464,8 +462,8 @@ namespace lms::scrobbling::listenBrainz
{ {
assert(context.listenBrainzUserName.empty()); assert(context.listenBrainzUserName.empty());
const std::optional<core::UUID> listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) }; const std::string listenBrainzToken{ utils::getListenBrainzToken(_db.getTLSSession(), context.userId) };
if (!listenBrainzToken) if (listenBrainzToken.empty())
{ {
onSyncEnded(context); onSyncEnded(context);
return; return;
@@ -474,7 +472,7 @@ namespace lms::scrobbling::listenBrainz
core::http::ClientGETRequestParameters request; core::http::ClientGETRequestParameters request;
request.priority = core::http::ClientRequestParameters::Priority::Low; request.priority = core::http::ClientRequestParameters::Priority::Low;
request.relativeUrl = "/1/validate-token"; 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) { request.onSuccessFunc = [this, &context](const Wt::Http::Message& msg) {
context.listenBrainzUserName = utils::parseValidateToken(msg.body()); context.listenBrainzUserName = utils::parseValidateToken(msg.body());
if (context.listenBrainzUserName.empty()) if (context.listenBrainzUserName.empty())
@@ -55,7 +55,7 @@ namespace lms::scrobbling::listenBrainz
private: private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); 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(); void enquePendingListens();
@@ -27,15 +27,18 @@
namespace lms::scrobbling::listenBrainz::utils 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) auto transaction{ session.createReadTransaction() };
return std::nullopt;
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) std::string parseValidateToken(std::string_view msgBody)
@@ -20,7 +20,7 @@
#pragma once #pragma once
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/objects/UserId.hpp" #include "database/objects/UserId.hpp"
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[listenbrainz] " << message) #define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[listenbrainz] " << message)
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::scrobbling::listenBrainz::utils 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); std::string parseValidateToken(std::string_view msgBody);
} // namespace lms::scrobbling::listenBrainz::utils } // namespace lms::scrobbling::listenBrainz::utils
-1
View File
@@ -55,7 +55,6 @@ target_include_directories(lmssubsonic PRIVATE
) )
target_link_libraries(lmssubsonic PRIVATE target_link_libraries(lmssubsonic PRIVATE
std::filesystem
lmsartwork lmsartwork
lmsaudio lmsaudio
lmsauth lmsauth
@@ -107,5 +107,4 @@ namespace lms::api::subsonic
{ {
return _isOpenSubsonicEnabled; return _isOpenSubsonicEnabled;
} }
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
@@ -77,5 +77,4 @@ namespace lms::api::subsonic
const ProtocolVersion _serverProtocolVersion; const ProtocolVersion _serverProtocolVersion;
const bool _isOpenSubsonicEnabled; const bool _isOpenSubsonicEnabled;
}; };
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+76 -65
View File
@@ -36,7 +36,6 @@
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp" #include "ParameterParsing.hpp"
#include "ProtocolVersion.hpp"
#include "RequestContext.hpp" #include "RequestContext.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#include "endpoints/AlbumSongLists.hpp" #include "endpoints/AlbumSongLists.hpp"
@@ -291,91 +290,48 @@ namespace lms::api::subsonic
const std::size_t requestId{ curRequestId++ }; const std::size_t requestId{ curRequestId++ };
TLSMonotonicMemoryResourceCleaner memoryResourceCleaner; 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() }; std::string requestPath{ request.pathInfo() };
if (core::stringUtils::stringEndsWith(requestPath, ".view")) if (core::stringUtils::stringEndsWith(requestPath, optionalSuffix))
requestPath.resize(requestPath.length() - 5); 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 try
{ {
if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end()) if (!handleMediaRetrievalRequest(requestPath, request, response))
{ handleRequest(requestPath, request, response);
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
db::User::pointer user; LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " to '" << requestPath << "' handled!");
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{};
} }
catch (const Error& e) 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() << "'"); 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() << "'");
Response resp{ Response::createFailedResponse(requestContext.getServerProtocolVersion(), e) };
resp.write(response.out(), requestContext.getResponseFormat());
response.setMimeType(std::string{ ResponseFormatToMimeType(requestContext.getResponseFormat()) });
} }
} }
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 try
{ {
// Media retrieval endpoints are always authenticated RequestContext requestContext{ request, _db.getTLSSession(), _config };
// Optimization: no need to reauth user for each continuation
// Media retrieval endpoints are always authenticated but we don't reauth user for a continuation
db::User::pointer user; db::User::pointer user;
if (!request.continuation()) if (!request.continuation())
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
requestContext.setUser(user); requestContext.setUser(user);
handler(requestContext, request, response); itStreamHandler->second(requestContext, request, response);
return true;
} }
catch (const UserNotAuthorizedError&) 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) db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
{ {
const auto& parameters{ request.getParameterMap() }; const auto& parameters{ request.getParameterMap() };
+4 -2
View File
@@ -18,6 +18,8 @@
*/ */
#pragma once #pragma once
#include <string>
#include <Wt/Http/Request.h> #include <Wt/Http/Request.h>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include <Wt/WResource.h> #include <Wt/WResource.h>
@@ -43,8 +45,8 @@ namespace lms::api::subsonic
private: private:
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; 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&)>; bool handleMediaRetrievalRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response);
void handleMediaRetrievalRequest(const MediaRetrievalHandlerFunc& handler, RequestContext& requestContext, 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); db::UserId authenticateUser(const Wt::Http::Request& request);
+100 -78
View File
@@ -24,6 +24,7 @@
#include <mutex> #include <mutex>
#include "core/Random.hpp" #include "core/Random.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
@@ -172,37 +173,48 @@ namespace lms::api::subsonic
if (!keywords.empty()) 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(); 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 else
{ {
ScanTracker<ArtistId>::ScanInfo scanInfo{ findArtists();
.clientAddress = context.getClientIpAddr(), }
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = artistOffset
};
if (ArtistId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) if (lastRetrievedId.isValid())
{ {
Artist::find( scanInfo.offset = artistOffset + artistCount;
context.getDbSession(), cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) { currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findArtists();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = artistOffset + artistCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
} }
} }
@@ -236,37 +248,48 @@ namespace lms::api::subsonic
if (!keywords.empty()) 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(); 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 else
{ {
ScanTracker<ReleaseId>::ScanInfo scanInfo{ findReleases();
.clientAddress = context.getClientIpAddr(), }
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = albumOffset
};
if (ReleaseId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) if (lastRetrievedId.isValid())
{ {
Release::find( scanInfo.offset = albumOffset + albumCount;
context.getDbSession(), cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) { currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findReleases();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = albumOffset + albumCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
} }
} }
@@ -301,36 +324,35 @@ namespace lms::api::subsonic
if (!keywords.empty()) if (!keywords.empty())
{ {
findTracks(); 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 else
{ {
ScanTracker<TrackId>::ScanInfo scanInfo{ findTracks();
.clientAddress = context.getClientIpAddr(), }
.clientName = std::string{ context.getClientName() },
.user = context.getUser()->getId(),
.library = mediaLibrary,
.offset = songOffset
};
if (TrackId cachedLastRetrievedId{ currentScansInProgress.extractLastRetrievedObjectId(scanInfo) }; cachedLastRetrievedId.isValid()) if (lastRetrievedId.isValid())
{ {
Track::find( scanInfo.offset = songOffset + songCount;
context.getDbSession(), cachedLastRetrievedId, songCount, [&](const Track::pointer& track) { currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
},
mediaLibrary);
lastRetrievedId = cachedLastRetrievedId;
}
else
{
findTracks();
}
if (lastRetrievedId.isValid())
{
scanInfo.offset = songOffset + songCount;
currentScansInProgress.setObjectId(scanInfo, lastRetrievedId);
}
} }
} }
@@ -19,8 +19,11 @@
#include "responses/DiscTitle.hpp" #include "responses/DiscTitle.hpp"
#include "database/objects/Artwork.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "CoverArtId.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium) 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("disc", medium->getPosition() ? *medium->getPosition() : 0);
discTitleNode.setAttribute("title", medium->getName()); 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; return discTitleNode;
} }
+1
View File
@@ -49,6 +49,7 @@ add_executable(lms
ui/explore/ReleasesView.cpp ui/explore/ReleasesView.cpp
ui/explore/ReleaseView.cpp ui/explore/ReleaseView.cpp
ui/explore/ReleaseTypes.cpp ui/explore/ReleaseTypes.cpp
ui/explore/TrackArtistTypeSelector.cpp
ui/explore/TrackCollector.cpp ui/explore/TrackCollector.cpp
ui/explore/TrackListHelpers.cpp ui/explore/TrackListHelpers.cpp
ui/explore/TrackListView.cpp ui/explore/TrackListView.cpp
+43 -3
View File
@@ -17,6 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <filesystem>
#include <thread> #include <thread>
#include <Wt/WApplication.h> #include <Wt/WApplication.h>
@@ -110,19 +111,58 @@ namespace lms
throw core::LmsException{ "Invalid config value for 'jukebox-audio-backend'" }; 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() }; core::IConfig& config{ *core::Service<core::IConfig>::get() };
std::vector<std::string> args; std::vector<std::string> args;
const std::filesystem::path wtConfigPath{ config.getPath("working-dir", "/var/lms") / "wt_config.xml" }; const std::filesystem::path 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 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(execPath);
args.push_back("--config=" + wtConfigPath.string()); 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("--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", "/") }); args.push_back("--deploy-path=" + std::string{ config.getString("deploy-path", "/") });
if (!wtResourcesPath.empty()) if (!wtResourcesPath.empty())
args.push_back("--resources-dir=" + wtResourcesPath.string()); args.push_back("--resources-dir=" + wtResourcesPath.string());
+14 -12
View File
@@ -32,6 +32,8 @@
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -144,7 +146,7 @@ namespace lms::ui
addField(ListenBrainzTokenField); addField(ListenBrainzTokenField);
setValidator(SubsonicTokenField, createUUIDValidator()); setValidator(SubsonicTokenField, createUUIDValidator());
setValidator(ListenBrainzTokenField, createUUIDValidator()); setValidator(ListenBrainzTokenField, createMandatoryValidator());
if (_authPasswordService) if (_authPasswordService)
{ {
@@ -284,14 +286,14 @@ namespace lms::ui
if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) }) if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) })
user.modify()->setFeedbackBackend(_feedbackBackendModel->getValue(*feedbackBackendRow)); 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)) }) if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromString(valueText(ScrobblingBackendField)) })
user.modify()->setScrobblingBackend(_scrobblingBackendModel->getValue(*scrobblingBackendRow)); 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()) if (_authPasswordService && !valueText(PasswordField).empty())
@@ -390,8 +392,8 @@ namespace lms::ui
if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) }) if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) })
setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow)); setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow));
if (auto listenBrainzToken{ user->getListenBrainzToken() }) if (const auto listenBrainzToken{ user->getListenBrainzToken() }; !listenBrainzToken.empty())
setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() })); setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken }));
{ {
const bool usesListenBrainz{ user->getScrobblingBackend() == db::ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == db::FeedbackBackend::ListenBrainz }; 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); _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name);
_artistRelationshipsModel = std::make_shared<ArtistRelationshipsModel>(); _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.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.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.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.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.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.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.remixer", 2), db::TrackArtistLinkType::Remixer);
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>(); _transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
+14 -14
View File
@@ -407,17 +407,17 @@ namespace lms::ui::utils
}); });
}; };
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer"); addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor"); addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist"); addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer"); addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer"); addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer"); addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.producer");
addPerformerArtists(); addPerformerArtists();
if (!rolelessPerformers.empty()) 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); artistMap[performersStr] = std::move(rolelessPerformers);
} }
@@ -458,17 +458,17 @@ namespace lms::ui::utils
}); });
}; };
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer"); addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor"); addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist"); addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer"); addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer"); addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer"); addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.producer");
addPerformerArtists(); addPerformerArtists();
if (!rolelessPerformers.empty()) 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); artistMap[performersStr] = std::move(rolelessPerformers);
} }
+2
View File
@@ -30,6 +30,8 @@
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/UUID.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/auth/IAuthTokenService.hpp" #include "services/auth/IAuthTokenService.hpp"
+10 -10
View File
@@ -30,16 +30,16 @@ namespace lms::ui
ArtistTypeSelector(ArtistType defaultArtistType) ArtistTypeSelector(ArtistType defaultArtistType)
: DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artists.template.artist-type-selector"), 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-all", Wt::WString::tr("Lms.Explore.all-artists"), AllArtistsTag{});
bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.Artists.release-artist", 2), ReleaseArtistsTag{}); bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.release-artist", 2), ReleaseArtistsTag{});
bindItem("artist-type-track-artist", Wt::WString::trn("Lms.Explore.Artists.track-artist", 2), db::TrackArtistLinkType::Artist); 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.Artists.composer", 2), db::TrackArtistLinkType::Composer); bindItem("artist-type-composer", Wt::WString::trn("Lms.Explore.composer", 2), db::TrackArtistLinkType::Composer);
bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor); bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.conductor", 2), db::TrackArtistLinkType::Conductor);
bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist); bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.lyricist", 2), db::TrackArtistLinkType::Lyricist);
bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer); bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.mixer", 2), db::TrackArtistLinkType::Mixer);
bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer); bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.performer", 2), db::TrackArtistLinkType::Performer);
bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer); bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.producer", 2), db::TrackArtistLinkType::Producer);
bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer); bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.remixer", 2), db::TrackArtistLinkType::Remixer);
} }
}; };
} // namespace lms::ui } // namespace lms::ui
+67 -14
View File
@@ -19,7 +19,10 @@
#include "ArtistView.hpp" #include "ArtistView.hpp"
#include <array>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WStackedWidget.h>
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -30,6 +33,8 @@
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
#include "services/recommendation/IRecommendationService.hpp" #include "services/recommendation/IRecommendationService.hpp"
@@ -40,6 +45,7 @@
#include "LmsApplicationException.hpp" #include "LmsApplicationException.hpp"
#include "PlayQueueController.hpp" #include "PlayQueueController.hpp"
#include "ReleaseHelpers.hpp" #include "ReleaseHelpers.hpp"
#include "TrackArtistTypeSelector.hpp"
#include "TrackListHelpers.hpp" #include "TrackListHelpers.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "common/InfiniteScrollingContainer.hpp" #include "common/InfiniteScrollingContainer.hpp"
@@ -272,30 +278,77 @@ namespace lms::ui
void Artist::refreshAppearsOnReleases() void Artist::refreshAppearsOnReleases()
{ {
_appearsOnReleases.clear();
_appearsOnReleaseContainer = {}; _appearsOnReleaseContainer = {};
db::Release::FindParameters params; auto addToReleaseContainer = [&](std::optional<db::TrackArtistLinkType> linkType) -> bool {
params.setTrackArtist(_artistId); db::Release::FindParameters params;
params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
db::Release::find(LmsApp->getDbSession(), params, [this](const db::Release::pointer& release) { core::EnumSet<db::TrackArtistLinkType> linkTypes;
if (!release->hasArtist(_artistId)) if (linkType)
_appearsOnReleaseContainer.releases.push_back(release->getId()); 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")) }; if (addToReleaseContainer(type) && type)
releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.Artist.appears-on")); foundTypes.insert(*type);
_appearsOnReleaseContainer.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); }
_appearsOnReleaseContainer.container->onRequestElements.connect(this, [this] {
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 }); 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 else
{
bindEmpty("appears-on-releases"); bindEmpty("appears-on-releases");
}
} }
void Artist::refreshNonReleaseTracks() void Artist::refreshNonReleaseTracks()
+4 -1
View File
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <map> #include <map>
#include <optional>
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
@@ -31,6 +32,7 @@
#include "ReleaseHelpers.hpp" #include "ReleaseHelpers.hpp"
#include "ReleaseTypes.hpp" #include "ReleaseTypes.hpp"
#include "common/Template.hpp" #include "common/Template.hpp"
#include "database/objects/Types.hpp"
namespace lms::db namespace lms::db
{ {
@@ -76,7 +78,8 @@ namespace lms::ui
std::vector<db::ReleaseId> releases; std::vector<db::ReleaseId> releases;
}; };
std::map<ReleaseType, ReleaseContainer> _releaseContainers; std::map<ReleaseType, ReleaseContainer> _releaseContainers;
ReleaseContainer _appearsOnReleaseContainer{}; std::map<std::optional<db::TrackArtistLinkType>, std::vector<db::ReleaseId>> _appearsOnReleases;
ReleaseContainer _appearsOnReleaseContainer;
InfiniteScrollingContainer* _trackContainer{}; InfiniteScrollingContainer* _trackContainer{};
db::ArtistId _artistId{}; db::ArtistId _artistId{};
std::string _artistName; 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