diff --git a/CMakeLists.txt b/CMakeLists.txt index d993e9cc..ffb277c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/Dockerfile-release b/Dockerfile-release index f4cbd190..78b536a1 100644 --- a/Dockerfile-release +++ b/Dockerfile-release @@ -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 diff --git a/INSTALL.md b/INSTALL.md index b5d795b9..0e38a00f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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).
+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. diff --git a/README.md b/README.md index e5304ed8..823d520c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/approot/artist.xml b/approot/artist.xml index 00c4f3a0..5dc68238 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -50,4 +50,32 @@ + +
+

${tr:Lms.Explore.Artist.appears-on}${artist-type class="ms-2 d-inline-block"}

+ ${releases} +
+
+ + + + + + diff --git a/approot/messages.xml b/approot/messages.xml index 1bcfd5f8..f9484c1b 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -196,9 +196,19 @@ Add filter All +All artists +All roles Artists Bitrate Codec + + Composer + Composers + + + Conductor + Conductors + Copyright Cover art Download @@ -206,26 +216,54 @@ Filter added Filters Label + + Lyricist + Lyricists + Music Library + + Mixer + Mixers + Most played MusicBrainz Artist MusicBrainz Release + + Performer + Performers + Play Play last Play next Play shuffled Play count + + Producer + Producers + Random Recently added Recently modified Recently played + + Album artist + Album artists + Release information Release type Albums + + Remixer + Remixers + Search Star Star {1} Starred + + Track artist + Track artists + Track information Lyrics Playlists @@ -241,45 +279,6 @@ Biography Similar artists - -All artists - - Composer - Composers - - - Conductor - Conductors - - - Lyricist - Lyricists - - - Mixer - Mixers - - - Performer - Performers - - - Producer - Producers - - - Album artist - Album artists - - - Remixer - Remixers - - - Track artist - Track artists - - Copyright Disc {1} diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 75a7c8e1..81b586f8 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -196,9 +196,19 @@ Añadir filtro Todos +Todos los artistas +Todos los roles Artistas Bitrate Codec + + Compositor + Compositores + + + Director de orquesta + Directores de orquesta + Derechos de autor Carátula Descargar @@ -206,26 +216,54 @@ Filtro añadido Filtros Sello discográfico + + Letrista + Letristas + Biblioteca musical + + Mezclador + Mezcladores + Reproducidos frecuentemente Artista de MusicBrainz Álbum de MusicBrainz + + Intérprete + Intérpretes + Reproducir Reproducir al final Reproducir a continuación Reproducción aleatoria Número de reproducciones + + Productor + Productores + Aleatorio Añadidos recientemente Modificados recientemente Reproducidos recientemente + + Artista del álbum + Artistas del álbum + Información sobre el álbum Tipo de publicación Álbumes + + Remezclador + Remezcladores + Buscar Añadir a favoritos Agregar {1} a favoritos Favoritos + + Artista de la pista + Artistas de la pista + Información sobre la pista Letras Listas de reproducción @@ -241,45 +279,6 @@ Biografía Artistas similares - -Todos los artistas - - Compositor - Compositores - - - Director de orquesta - Directores de orquesta - - - Letrista - Letristas - - - Mezclador - Mezcladores - - - Intérprete - Intérpretes - - - Productor - Productores - - - Artista del álbum - Artistas del álbum - - - Remezclador - Remezcladores - - - Artista de la pista - Artistas de la pista - - Copyright Disco {1} diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index e77abe2b..af320285 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -196,9 +196,19 @@ Ajouter filtre Tous +Tous les artistes +Tous les rôles Artistes Bitrate Codec + + Compositeur + Compositeurs + + + Chef d'orchestre + Chefs d'orchestre + Droits d'auteur Pochette Télécharger @@ -206,26 +216,54 @@ Filtre ajouté Filtres Label + + Parolier + Paroliers + Bibliothèque musicale + + Mixeur + Mixeurs + Plus joués MusicBrainz Artist MusicBrainz Release + + Interprète + Interprètes + Jouer Jouer en dernier Jouer ensuite Jouer aléatoirement Nombre d'écoutes + + Producteur + Producteurs + Aléatoire Ajoutés récemment Modifiés récemment Joués récemment + + Artiste d'album + Artistes d'album + Informations sur l'album Type de publication Albums + + Remixeur + Remixeurs + Rechercher Ajouter aux favoris Ajouter {1} aux favoris Favoris + + Artiste de piste + Artistes de piste + Informations sur la piste Paroles Playlists @@ -241,45 +279,6 @@ Biographie Artistes similaires - -Tous les artistes - - Compositeur - Compositeurs - - - Chef d'orchestre - Chefs d'orchestre - - - Parolier - Paroliers - - - Mixeur - Mixeurs - - - Interprète - Interprètes - - - Producteur - Producteurs - - - Artiste d'album - Artistes d'album - - - Remixeur - Remixeurs - - - Artiste de piste - Artistes de piste - - Copyright Disque {1} diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 5eba39d8..0254d1cc 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -196,9 +196,19 @@ Aggiungi filtro Tutti +Tutti gli artisti +Tutti i ruoli Artisti Bitrate Codec + + Compositore + Compositori + + + Direttore d'orchestra + Direttori d'orchestra + Diritti d'autore Copertina Download @@ -206,26 +216,54 @@ Filtro aggiunto Filtri Etichetta + + Paroliere + Parolieri + Libreria musicale + + Tecnico del mixaggio + Tecnici del mixaggio + Più ascoltati Artista MusicBrainz Album MusicBrainz + + Interprete + Interpreti + Riproduci Aggiungi alla coda di riproduzione Riproduci successiva Riproduci mischiando Conteggio riproduzioni + + Produttore + Produttori + Casuali Aggiunti di recente Modificati di recente Riprodotti di recente + + Artista dell'album + Artisti dell'album + Informazioni sull'album Tipo di pubblicazione Album + + Remixer + Remixers + Ricerca Aggiungi ai preferiti Aggiungi {1} ai preferiti Preferiti + + Artista della traccia + Artisti della traccia + Informazioni sulla traccia Testo Playlist @@ -241,45 +279,6 @@ Biografia Artisti simili - -Tutti gli artisti - - Compositore - Compositori - - - Direttore d'orchestra - Direttori d'orchestra - - - Paroliere - Parolieri - - - Tecnico del mixaggio - Tecnici del mixaggio - - - Interprete - Interpreti - - - Produttore - Produttori - - - Artista dell'album - Artisti dell'album - - - Remixer - Remixers - - - Artista della traccia - Artisti della traccia - - Copyright Disco {1} diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 08181d06..48dd4b72 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -213,9 +213,21 @@ Dodaj filtr Wszystko +Wszyscy artyści +Wszystkie role Artyści Przepływność Kodek + + Kompozytor + Kompozytorzy + Kompozytorów + + + Dyrygent + Dyrygenci + Dyrygentów + Prawa autorskie Okładka Pobierz @@ -223,26 +235,61 @@ Filtr dodany Filtry Wytwórnia + + Tekściarz + Tekściarze + Tekściarzy + Biblioteka muzyczna + + Mikser + Mikserzy + Mikserów + Najczęściej odtwarzane Artysta w MusicBrainz Wydanie w MusicBrainz + + Wykonawca + Wykonawcy + Wykonawców + Odtwarzaj Odtwórz na końcu Odtwórz po obecnym Odtwarzaj losowo Liczba odtworzeń + + Producent + Producenci + Producentów + Losowo Ostatnio dodane Ostatnio zmienione Ostatnio odtwarzane + + Artysta albumu + Artyści albumu + Artystów albumu + Informacje o wydaniu Typ wydania Albumy + + Remikser + Remikserzy + Remikserów + Szukaj Wyróżnij Dodaj {1} do ulubionych Wyróżnione + + Artysta utworu + Artyści utworu + Artystów utworu + Informacje o ścieżce Tekst Listy odtwarzania @@ -258,54 +305,6 @@ Biografia Podobni artyści - -Wszyscy artyści - - Kompozytor - Kompozytorzy - Kompozytorzy - - - Dyrygent - Dyrygenci - Dyrygenci - - - Tekściarz - Tekściarze - Tekściarze - - - Realizator - Realizatorzy - Realizatorzy - - - Wykonawca - Wykonawcy - Wykonawcy - - - Producent - Producenci - Producenci - - - Artysta z albumu - Artyści z albumu - Artyści z albumu - - - Remixer - Remikserzy - Remikserzy - - - Artysta ze ścieżki - Artyści ze ścieżki - Artyści ze ścieżki - - Prawa autorskie Dysk {1} diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index ee81802d..810d2c4a 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -194,9 +194,13 @@ 添加过滤器 全部 +所有艺术家 +所有角色 歌手 比特率 编码器 +作曲者 +指挥 版权所有 封面 下载 @@ -204,26 +208,33 @@ 过滤器已添加 过滤器 厂牌 +作词者 音乐库 +混音师 播放最多 MusicBrainz 歌手 MusicBrainz 专辑 +表演者 播放 最后播放 下一首播放 随机播放 播放次数 +制作人 随机 最近添加 最近更改 最近播放 +专辑艺术家 专辑信息 专辑类型 专辑 +混音师 搜索 收藏 将 {1} 加入收藏 已收藏 +曲目艺术家 曲目信息 歌词 播放列表 @@ -239,18 +250,6 @@ 简介 相似歌手 - -所有艺术家 -作曲者 -指挥 -作词者 -混音师 -表演者 -制作人 -专辑艺术家 -混音师 -曲目艺术家 - 版权所有 唱片 {1} diff --git a/cmake/modules/FindFilesystem.cmake b/cmake/modules/FindFilesystem.cmake deleted file mode 100755 index bdccdb95..00000000 --- a/cmake/modules/FindFilesystem.cmake +++ /dev/null @@ -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 ` 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 `. 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(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() diff --git a/src/libs/audio/CMakeLists.txt b/src/libs/audio/CMakeLists.txt index 08cca0f5..66b3df56 100644 --- a/src/libs/audio/CMakeLists.txt +++ b/src/libs/audio/CMakeLists.txt @@ -44,7 +44,6 @@ target_include_directories(lmsaudio PRIVATE target_link_libraries(lmsaudio PUBLIC lmscore - std::filesystem ) target_link_libraries(lmsaudio PRIVATE diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index d8ddf79d..6daf6301 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -54,7 +54,6 @@ target_link_libraries(lmscore PRIVATE ) target_link_libraries(lmscore PUBLIC - std::filesystem Wt::Wt ) diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 942a6c29..8c0055c7 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -64,7 +64,6 @@ target_link_libraries(lmsdatabase PRIVATE target_link_libraries(lmsdatabase PUBLIC lmscore - std::filesystem Wt::Dbo ) diff --git a/src/libs/database/include/database/objects/User.hpp b/src/libs/database/include/database/objects/User.hpp index 88dd21c0..29618a7f 100644 --- a/src/libs/database/include/database/objects/User.hpp +++ b/src/libs/database/include/database/objects/User.hpp @@ -27,7 +27,6 @@ #include #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& 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 getListenBrainzToken() const { return core::UUID::fromString(_listenbrainzToken); } + std::string_view getListenBrainzToken() const { return _listenbrainzToken; } template void persist(Action& a) diff --git a/src/libs/image/CMakeLists.txt b/src/libs/image/CMakeLists.txt index 53435e19..f742492d 100644 --- a/src/libs/image/CMakeLists.txt +++ b/src/libs/image/CMakeLists.txt @@ -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") diff --git a/src/libs/services/artwork/CMakeLists.txt b/src/libs/services/artwork/CMakeLists.txt index 3acefa6d..2d78ad42 100644 --- a/src/libs/services/artwork/CMakeLists.txt +++ b/src/libs/services/artwork/CMakeLists.txt @@ -22,5 +22,4 @@ target_link_libraries(lmsartwork PUBLIC lmsdatabase lmsimage lmscore - std::filesystem ) diff --git a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp index 4e9133a0..0ba352a2 100644 --- a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp @@ -118,13 +118,13 @@ namespace lms::feedback::listenBrainz return; } - const std::optional 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 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()) diff --git a/src/libs/services/feedback/impl/listenbrainz/Utils.cpp b/src/libs/services/feedback/impl/listenbrainz/Utils.cpp index 6f9932d4..90057982 100644 --- a/src/libs/services/feedback/impl/listenbrainz/Utils.cpp +++ b/src/libs/services/feedback/impl/listenbrainz/Utils.cpp @@ -27,15 +27,18 @@ namespace lms::feedback::listenBrainz::utils { - std::optional 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) diff --git a/src/libs/services/feedback/impl/listenbrainz/Utils.hpp b/src/libs/services/feedback/impl/listenbrainz/Utils.hpp index d2790cef..2b131a16 100644 --- a/src/libs/services/feedback/impl/listenbrainz/Utils.hpp +++ b/src/libs/services/feedback/impl/listenbrainz/Utils.hpp @@ -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 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 diff --git a/src/libs/services/recommendation/CMakeLists.txt b/src/libs/services/recommendation/CMakeLists.txt index cd170732..21e89b3a 100644 --- a/src/libs/services/recommendation/CMakeLists.txt +++ b/src/libs/services/recommendation/CMakeLists.txt @@ -21,9 +21,7 @@ target_include_directories(lmsrecommendation PRIVATE ) target_link_libraries(lmsrecommendation PRIVATE - lmssom - std::filesystem ) target_link_libraries(lmsrecommendation PUBLIC diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 69ef7c3e..ce4a8d6d 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -57,7 +57,6 @@ target_link_libraries(lmsscanner PRIVATE target_link_libraries(lmsscanner PUBLIC lmsdatabase - std::filesystem Wt::Wt ) diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index 6895593d..62da3092 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -291,34 +291,32 @@ namespace lms::scrobbling::listenBrainz return; } - const std::optional 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 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()) diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index 3bd41c2f..de8b4bbb 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -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(); diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp index 230f9df1..7a33db20 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp @@ -27,15 +27,18 @@ namespace lms::scrobbling::listenBrainz::utils { - std::optional 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) diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp index 6bd75648..816f7e80 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp @@ -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 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 diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 25f4cf89..2fd1576d 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -55,7 +55,6 @@ target_include_directories(lmssubsonic PRIVATE ) target_link_libraries(lmssubsonic PRIVATE - std::filesystem lmsartwork lmsaudio lmsauth diff --git a/src/libs/subsonic/impl/RequestContext.cpp b/src/libs/subsonic/impl/RequestContext.cpp index 8ca811ac..755aac4b 100644 --- a/src/libs/subsonic/impl/RequestContext.cpp +++ b/src/libs/subsonic/impl/RequestContext.cpp @@ -107,5 +107,4 @@ namespace lms::api::subsonic { return _isOpenSubsonicEnabled; } - } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index c5cc1568..fe4c496c 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -77,5 +77,4 @@ namespace lms::api::subsonic const ProtocolVersion _serverProtocolVersion; const bool _isOpenSubsonicEnabled; }; - } // namespace lms::api::subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index e9d5f7fe..21df8265 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -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(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(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(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; + 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() }; diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index f65c92c3..0ee33fa9 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -18,6 +18,8 @@ */ #pragma once +#include + #include #include #include @@ -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 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); diff --git a/src/libs/subsonic/impl/endpoints/Searching.cpp b/src/libs/subsonic/impl/endpoints/Searching.cpp index c8119c9d..e6ab740c 100644 --- a/src/libs/subsonic/impl/endpoints/Searching.cpp +++ b/src/libs/subsonic/impl/endpoints/Searching.cpp @@ -24,6 +24,7 @@ #include #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::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::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::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::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::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::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); } } diff --git a/src/libs/subsonic/impl/responses/DiscTitle.cpp b/src/libs/subsonic/impl/responses/DiscTitle.cpp index a3f1d565..355409c5 100644 --- a/src/libs/subsonic/impl/responses/DiscTitle.cpp +++ b/src/libs/subsonic/impl/responses/DiscTitle.cpp @@ -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& 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; } diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 699d0662..a2dec023 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -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 diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 61890196..51f81923 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -17,6 +17,7 @@ * along with LMS. If not, see . */ +#include #include #include @@ -110,19 +111,58 @@ namespace lms throw core::LmsException{ "Invalid config value for 'jukebox-audio-backend'" }; } - std::vector 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 generateWtConfig(const std::string& execPath) { core::IConfig& config{ *core::Service::get() }; std::vector 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()); diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 36fe516b..4be8126c 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -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->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(); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index f2241c3c..f61dd1f7 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -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); } diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 8ab44a40..85a0505d 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -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" diff --git a/src/lms/ui/explore/ArtistTypeSelector.hpp b/src/lms/ui/explore/ArtistTypeSelector.hpp index aaa56726..aceca46d 100644 --- a/src/lms/ui/explore/ArtistTypeSelector.hpp +++ b/src/lms/ui/explore/ArtistTypeSelector.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 diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 98096848..c3073f1f 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -19,7 +19,10 @@ #include "ArtistView.hpp" +#include + #include +#include #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 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 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, 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 foundTypes; + for (const auto type : types) { - Wt::WTemplate* releaseContainer{ bindNew("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("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 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("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.appears-on-container")) }; + appearsOnContainer->addFunction("tr", &Wt::WTemplate::Functions::tr); + + _appearsOnReleaseContainer.container = appearsOnContainer->bindNew("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("artist-type", defaultType, foundTypes) }; + trackArtistTypeSelector->itemSelected.connect([this](std::optional newArtistLinkType) { + _appearsOnReleaseContainer.releases = _appearsOnReleases[newArtistLinkType]; + _appearsOnReleaseContainer.container->reset(); + }); } else - { bindEmpty("appears-on-releases"); - } } void Artist::refreshNonReleaseTracks() diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index ebd8877c..47d37c63 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #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 releases; }; std::map _releaseContainers; - ReleaseContainer _appearsOnReleaseContainer{}; + std::map, std::vector> _appearsOnReleases; + ReleaseContainer _appearsOnReleaseContainer; InfiniteScrollingContainer* _trackContainer{}; db::ArtistId _artistId{}; std::string _artistName; diff --git a/src/lms/ui/explore/TrackArtistTypeSelector.cpp b/src/lms/ui/explore/TrackArtistTypeSelector.cpp new file mode 100644 index 00000000..6b41f104 --- /dev/null +++ b/src/lms/ui/explore/TrackArtistTypeSelector.cpp @@ -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 . + */ + +#include "TrackArtistTypeSelector.hpp" + +#include + +namespace lms::ui +{ + TrackArtistTypeSelector::TrackArtistTypeSelector(std::optional defaultRole, core::EnumSet types) + : DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artist.template.track-artist-type-selector"), defaultRole } + { + struct Entry + { + std::string var; + Wt::WString title; + std::optional linkType; + core::LiteralString condition; + }; + + auto bindItemWithCondition = [&](const std::string& var, const Wt::WString& title, std::optional 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 diff --git a/src/lms/ui/explore/TrackArtistTypeSelector.hpp b/src/lms/ui/explore/TrackArtistTypeSelector.hpp new file mode 100644 index 00000000..380767cb --- /dev/null +++ b/src/lms/ui/explore/TrackArtistTypeSelector.hpp @@ -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 . + */ + +#pragma once + +#include "core/EnumSet.hpp" + +#include "database/objects/Types.hpp" + +#include "DropDownMenuSelector.hpp" + +namespace lms::ui +{ + class TrackArtistTypeSelector : public DropDownMenuSelector> + { + public: + TrackArtistTypeSelector(std::optional defaultRole, core::EnumSet types); + }; +} // namespace lms::ui