From 3d5e2d3563c18729779ebaa7f05786bfcfba21fb Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 2 Mar 2026 22:29:22 +0100 Subject: [PATCH 01/14] Removed useless filesystem cmake script --- CMakeLists.txt | 1 - cmake/modules/FindFilesystem.cmake | 230 ------------------ src/libs/audio/CMakeLists.txt | 1 - src/libs/core/CMakeLists.txt | 1 - src/libs/database/CMakeLists.txt | 1 - src/libs/image/CMakeLists.txt | 1 - src/libs/services/artwork/CMakeLists.txt | 1 - .../services/recommendation/CMakeLists.txt | 2 - src/libs/services/scanner/CMakeLists.txt | 1 - src/libs/subsonic/CMakeLists.txt | 1 - 10 files changed, 240 deletions(-) delete mode 100755 cmake/modules/FindFilesystem.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d993e9cc..46e7edff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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/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/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/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/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 From 4c7ad485bf9e421508ae018bdbcad28e4ad88a70 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 8 Mar 2026 21:30:11 +0100 Subject: [PATCH 02/14] Added basic checks to ensure configured directories in lms.conf exist, fixes #828 --- INSTALL.md | 8 ++++---- src/lms/main.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index b5d795b9..dbe9146a 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 the relevant settings like 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/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()); From 0c9f972ef2a8909e984e7d4034d23331ab6d29f5 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 9 Mar 2026 21:31:19 +0100 Subject: [PATCH 03/14] Added some details about some supported non standard tags --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5304ed8..0cda54ad 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,15 @@ $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_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid`, +**Sort order variants**: +* `conductorssort`, `producerssort`, `mixerssort`, `remixerssort`, `lyricistssort`, `albumartistssort` + ### 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. From 0d0468427a570072960cad5fc5ba491e7002af3c Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 9 Mar 2026 21:35:55 +0100 Subject: [PATCH 04/14] Fixed typo --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cda54ad..379a47ab 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ $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_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid`, +* `musicbrainz_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid` + **Sort order variants**: * `conductorssort`, `producerssort`, `mixerssort`, `remixerssort`, `lyricistssort`, `albumartistssort` From 10707d8f842e215ec01abba33b608ff2e7cf058d Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 9 Mar 2026 21:37:33 +0100 Subject: [PATCH 05/14] Better formatting --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 379a47ab..f1da133e 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,8 @@ $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_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid` - -**Sort order variants**: -* `conductorssort`, `producerssort`, `mixerssort`, `remixerssort`, `lyricistssort`, `albumartistssort` +* **MusicBrainz identifiers** for artist relationships: `musicbrainz_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid` +* **Sort order variants**: `conductorssort`, `producerssort`, `mixerssort`, `remixerssort`, `lyricistssort`, `albumartistssort` ### Album track grouping The recommended way to group tracks within an album is to use the `musicbrainz_albumid` tag. From f9ae188be38a5e58d76213b0488a8cca4b66e167 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 10 Mar 2026 15:51:49 +0100 Subject: [PATCH 06/14] Subsonic API: fixed unwanted termination when bad client parameters are received --- src/libs/subsonic/impl/RequestContext.cpp | 1 - src/libs/subsonic/impl/RequestContext.hpp | 1 - src/libs/subsonic/impl/SubsonicResource.cpp | 141 +++++++++++--------- src/libs/subsonic/impl/SubsonicResource.hpp | 6 +- 4 files changed, 80 insertions(+), 69 deletions(-) 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); From 52d31dbec4ab17c5e1224b60aca237a28f0b03de Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 10 Mar 2026 16:32:38 +0100 Subject: [PATCH 07/14] Subsonic API: can now search albums and artists using mbid, fixes #826 --- .../subsonic/impl/endpoints/Searching.cpp | 178 ++++++++++-------- 1 file changed, 100 insertions(+), 78 deletions(-) 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); } } From 82468be9cc306cd9745ef77d57cf9295eca2c07d Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 10 Mar 2026 17:00:54 +0100 Subject: [PATCH 08/14] Minor style update --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index dbe9146a..0e38a00f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -99,7 +99,7 @@ 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 at `/etc/lms.conf`. It is recommended to edit this file and adjust the relevant settings like the working directory, Wt resources path, listen address, listen port, etc. +_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 From 086cbea03e6514b78846f70eefd8df53e058a094 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 10 Mar 2026 17:07:12 +0100 Subject: [PATCH 09/14] Removed UUID constraint on listenbrainz token, fixes #825 --- .../include/database/objects/User.hpp | 5 ++--- .../listenbrainz/FeedbacksSynchronizer.cpp | 12 +++++------ .../feedback/impl/listenbrainz/Utils.cpp | 15 ++++++++------ .../feedback/impl/listenbrainz/Utils.hpp | 4 ++-- .../impl/listenbrainz/ListensSynchronizer.cpp | 20 +++++++++---------- .../impl/listenbrainz/ListensSynchronizer.hpp | 2 +- .../scrobbling/impl/listenbrainz/Utils.cpp | 15 ++++++++------ .../scrobbling/impl/listenbrainz/Utils.hpp | 4 ++-- src/lms/ui/SettingsView.cpp | 12 ++++++----- src/lms/ui/admin/UserView.cpp | 2 ++ 10 files changed, 49 insertions(+), 42 deletions(-) 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/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/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/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 36fe516b..158064a3 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 }; 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" From 14e18bf227f77ff149caa8eb666b72c983f12c43 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Mar 2026 08:40:40 +0100 Subject: [PATCH 10/14] Added some details about some supported non standard tags, ref #824 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f1da133e..823d520c 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ $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_arrangerid`, `musicbrainz_composerid`, `musicbrainz_conductorid`, `musicbrainz_lyricistid`, `musicbrainz_mixerid`, `musicbrainz_producerid`, `musicbrainz_remixerid` -* **Sort order variants**: `conductorssort`, `producerssort`, `mixerssort`, `remixerssort`, `lyricistssort`, `albumartistssort` +* **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. From 535809f5764e2768611a046a534be7c87e7affed Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Mar 2026 16:15:07 +0100 Subject: [PATCH 11/14] Added an optional role filter for 'appears on' section the artist views, fixes #824 --- approot/artist.xml | 28 ++++++ approot/messages.xml | 77 ++++++++------- approot/messages_es.xml | 77 ++++++++------- approot/messages_fr.xml | 77 ++++++++------- approot/messages_it.xml | 77 ++++++++------- approot/messages_pl.xml | 95 +++++++++---------- approot/messages_zh.xml | 23 +++-- src/lms/CMakeLists.txt | 1 + src/lms/ui/SettingsView.cpp | 14 +-- src/lms/ui/Utils.cpp | 28 +++--- src/lms/ui/explore/ArtistTypeSelector.hpp | 20 ++-- src/lms/ui/explore/ArtistView.cpp | 81 +++++++++++++--- src/lms/ui/explore/ArtistView.hpp | 5 +- .../ui/explore/TrackArtistTypeSelector.cpp | 60 ++++++++++++ .../ui/explore/TrackArtistTypeSelector.hpp | 35 +++++++ 15 files changed, 436 insertions(+), 262 deletions(-) create mode 100644 src/lms/ui/explore/TrackArtistTypeSelector.cpp create mode 100644 src/lms/ui/explore/TrackArtistTypeSelector.hpp 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/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/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 158064a3..4be8126c 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -459,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/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 From c9906395d7b0be95b9556d12f8c4bd6d490032d9 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Mar 2026 17:56:50 +0100 Subject: [PATCH 12/14] OS API: added coverArt in discTitle responses --- src/libs/subsonic/impl/responses/DiscTitle.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) 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; } From 0fe4d7832cf93e06485f8f8804ef438e62b517c5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Mar 2026 19:48:47 +0100 Subject: [PATCH 13/14] Bumped version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 46e7edff..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/) From 351c412dd325d6d9730271f9487bb5cdb05ec1cd Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 14 Mar 2026 19:50:35 +0100 Subject: [PATCH 14/14] Bumped Wt version --- Dockerfile-release | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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