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