From 95bc649fc109bac68e4e29bf5262c35c61409f73 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Sep 2025 13:20:43 +0200 Subject: [PATCH 01/12] Fixed assert/potential bug when aborting podcast sync while being throttled --- src/libs/core/impl/http/SendQueue.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/core/impl/http/SendQueue.cpp b/src/libs/core/impl/http/SendQueue.cpp index bdbe8931..b2b1a35a 100644 --- a/src/libs/core/impl/http/SendQueue.cpp +++ b/src/libs/core/impl/http/SendQueue.cpp @@ -313,7 +313,7 @@ namespace lms::core::http LOG(DEBUG, "Throttling for " << duration.count() << " seconds"); _throttleTimer.expires_after(duration); - _throttleTimer.async_wait([this](const boost::system::error_code& ec) { + _throttleTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec) { if (ec == boost::asio::error::operation_aborted) LOG(DEBUG, "Throttle aborted"); else if (ec) @@ -322,7 +322,7 @@ namespace lms::core::http setState(State::Idle); if (!ec) sendNextQueuedRequest(); - }); + })); setState(State::Throttled); } From 741e7ae13b86b740e47dfe97604d15783c17785e Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Sep 2025 13:21:12 +0200 Subject: [PATCH 02/12] Do not update podcasts that are being marked for removal --- src/libs/services/podcast/impl/PodcastService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/services/podcast/impl/PodcastService.cpp b/src/libs/services/podcast/impl/PodcastService.cpp index f5e0400e..bb36b245 100644 --- a/src/libs/services/podcast/impl/PodcastService.cpp +++ b/src/libs/services/podcast/impl/PodcastService.cpp @@ -274,8 +274,8 @@ namespace lms::podcast // order is important, each step is done only when the previous one is done _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); - _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); + _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); _refreshSteps.emplace_back(std::make_unique(_refreshContext, onDoneCallback)); From dc59407b9e518959a6c3efb32753ee212b8d46e2 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 14 Sep 2025 13:21:26 +0200 Subject: [PATCH 03/12] Adjusted log --- src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp b/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp index ea685dcf..ba718688 100644 --- a/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp +++ b/src/libs/services/podcast/impl/steps/RefreshPodcastsStep.cpp @@ -79,9 +79,9 @@ namespace lms::podcast dbPodcast.modify()->setSubtitle(podcast.subtitle); dbPodcast.modify()->setSummary(podcast.summary); dbPodcast.modify()->setTitle(podcast.title); - if (dbPodcast->getImageUrl() != podcast.imageUrl) + if (std::string previousUrl{ dbPodcast->getImageUrl() }; !previousUrl.empty() && previousUrl != podcast.imageUrl) { - LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : image url changed from '" << dbPodcast->getImageUrl() << "' to '" << podcast.imageUrl << "'"); + LMS_LOG(PODCAST, INFO, "Podcast '" << podcast.title << "' : image url changed from '" << previousUrl << "' to '" << podcast.imageUrl << "'"); if (db::Artwork::pointer currentArtwork{ dbPodcast->getArtwork() }) removeArtwork(session, currentArtwork); From 87fd8bef3bf8af256ca1ede377eba3dbf8efb5c2 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Sep 2025 20:27:33 +0200 Subject: [PATCH 04/12] Added a fallback to accept musicbrainzartistid ins artist.nfo files, ref #750 --- CMakeLists.txt | 1 + src/libs/metadata/CMakeLists.txt | 1 + src/libs/metadata/impl/ArtistInfo.cpp | 61 +++++++++++++++--------- src/libs/metadata/test/ArtistInfo.cpp | 18 +++++++ src/libs/services/podcast/CMakeLists.txt | 5 +- 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a51a50e6..3ebbd18d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ find_package(Threads REQUIRED) find_package(Filesystem REQUIRED) find_package(Boost REQUIRED COMPONENTS system program_options iostreams) find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) +find_package(Pugixml CONFIG REQUIRED) # WT if (NOT Wt_FOUND) diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt index ea401ef1..1c3c2967 100644 --- a/src/libs/metadata/CMakeLists.txt +++ b/src/libs/metadata/CMakeLists.txt @@ -34,6 +34,7 @@ target_include_directories(lmsmetadata PRIVATE target_link_libraries(lmsmetadata PRIVATE lmsav PkgConfig::Taglib + pugixml::pugixml ) target_link_libraries(lmsmetadata PUBLIC diff --git a/src/libs/metadata/impl/ArtistInfo.cpp b/src/libs/metadata/impl/ArtistInfo.cpp index 85b4e96b..bcba6b88 100644 --- a/src/libs/metadata/impl/ArtistInfo.cpp +++ b/src/libs/metadata/impl/ArtistInfo.cpp @@ -19,14 +19,27 @@ #include "metadata/ArtistInfo.hpp" -#include -#include +#include #include "core/ILogger.hpp" +#include "core/LiteralString.hpp" #include "core/String.hpp" namespace lms::metadata { + namespace + { + std::optional getText(const pugi::xml_node& node, const core::LiteralString& tag) + { + std::optional res; + + if (const pugi::xml_node child{ node.child(tag.c_str()) }) + res = std::string_view{ child.child_value() }; + + return res; + } + } // namespace + std::span getSupportedArtistInfoFiles() { static const std::array files{ "artist.nfo" }; @@ -35,29 +48,33 @@ namespace lms::metadata ArtistInfo parseArtistInfo(std::istream& is) { - try + ArtistInfo artistInfo; + pugi::xml_document doc; + pugi::xml_parse_result result{ doc.load(is) }; + if (!result) { - ArtistInfo artistInfo; - - boost::property_tree::ptree root; - boost::property_tree::read_xml(is, root); - - const auto& artistNode{ root.get_child("artist") }; - - artistInfo.mbid = core::UUID::fromString(core::stringUtils::stringTrim(artistNode.get_optional("musicBrainzArtistID").value_or(""))); - artistInfo.name = core::stringUtils::stringTrim(artistNode.get_optional("name").value_or("")); - artistInfo.sortName = core::stringUtils::stringTrim(artistNode.get_optional("sortname").value_or("")); - artistInfo.type = core::stringUtils::stringTrim(artistNode.get_optional("type").value_or("")); - artistInfo.gender = core::stringUtils::stringTrim(artistNode.get_optional("gender").value_or("")); - artistInfo.disambiguation = core::stringUtils::stringTrim(artistNode.get_optional("disambiguation").value_or("")); - artistInfo.biography = artistNode.get_optional("biography").value_or(""); - - return artistInfo; + LMS_LOG(METADATA, ERROR, "Cannot read artist info xml: " << result.description()); + throw ArtistInfoParseException{ result.description() }; } - catch (boost::property_tree::ptree_error& error) + + const pugi::xml_node artistNode{ doc.child("artist") }; + if (!artistNode) + throw ArtistInfoParseException{ "No element found in artist info xml" }; + { - LMS_LOG(METADATA, ERROR, "Cannot read artist xml info: " << error.what()); - throw ArtistInfoParseException{ error.what() }; + auto mbid{ getText(artistNode, "musicBrainzArtistID") }; + if (!mbid.has_value()) + mbid = getText(artistNode, "musicbrainzartistid"); // lidarr seems to put this in lowercase + artistInfo.mbid = core::UUID::fromString(core::stringUtils::stringTrim(mbid.has_value() ? *mbid : "")); } + + artistInfo.name = core::stringUtils::stringTrim(getText(artistNode, "name").value_or("")); + artistInfo.sortName = core::stringUtils::stringTrim(getText(artistNode, "sortname").value_or("")); + artistInfo.type = core::stringUtils::stringTrim(getText(artistNode, "type").value_or("")); + artistInfo.gender = core::stringUtils::stringTrim(getText(artistNode, "gender").value_or("")); + artistInfo.disambiguation = core::stringUtils::stringTrim(getText(artistNode, "disambiguation").value_or("")); + artistInfo.biography = getText(artistNode, "biography").value_or(""); + + return artistInfo; } } // namespace lms::metadata \ No newline at end of file diff --git a/src/libs/metadata/test/ArtistInfo.cpp b/src/libs/metadata/test/ArtistInfo.cpp index 4410cf2b..aa84b87e 100644 --- a/src/libs/metadata/test/ArtistInfo.cpp +++ b/src/libs/metadata/test/ArtistInfo.cpp @@ -77,6 +77,24 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl ASSERT_EQ(artistInfo.biography, "DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.\r\n\r\nHe moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh."); } + TEST(ArtistInfo, basic_musicbrainzartistid) + { + std::istringstream is{ R"( + + Tim Taylor + 38811c52-85e3-4e2e-3319-ab7d9f2cfa5b + Taylor, Tim + Timothy Taylor +)" }; + + const ArtistInfo artistInfo{ parseArtistInfo(is) }; + + EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b")); + EXPECT_EQ(artistInfo.name, "Tim Taylor"); + ASSERT_EQ(artistInfo.sortName, "Taylor, Tim"); + ASSERT_EQ(artistInfo.disambiguation, "Timothy Taylor"); + } + TEST(ArtistInfo, trim) { std::istringstream is{ R"( diff --git a/src/libs/services/podcast/CMakeLists.txt b/src/libs/services/podcast/CMakeLists.txt index 889d2b53..54b4a77f 100644 --- a/src/libs/services/podcast/CMakeLists.txt +++ b/src/libs/services/podcast/CMakeLists.txt @@ -1,5 +1,3 @@ -pkg_check_modules(PUGIXML REQUIRED IMPORTED_TARGET pugixml) - add_library(lmspodcast STATIC impl/steps/CheckForMissingFilesStep.cpp impl/steps/ClearTmpDirectoryStep.cpp @@ -22,13 +20,12 @@ target_include_directories(lmspodcast INTERFACE target_include_directories(lmspodcast PRIVATE include impl - ${PUGIXML_INCLUDE_DIRS} ) target_link_libraries(lmspodcast PRIVATE lmscore lmsimage - PkgConfig::PUGIXML + pugixml::pugixml ) target_link_libraries(lmspodcast PUBLIC From be75d46dc37fbf0b09539e7154be5fa48d4bff31 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Sep 2025 20:31:45 +0200 Subject: [PATCH 05/12] Now trying to match existing 'fanart' file to associate an artist image in the artist.nfo folder, ref #750 --- .../scanner/impl/steps/ScanStepAssociateArtistImages.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index 2a3a321c..e2fa7aea 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -109,8 +109,9 @@ namespace lms::scanner db::ArtistInfo::find(session, artistId, [&](const db::ArtistInfo::pointer& artistInfo) { fileInfoPaths.push_back(artistInfo->getAbsoluteFilePath()); + // TODO make these names configurable if (!image) - image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), std::array{ "thumb", "folder" }); + image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), std::array{ "thumb", "folder", "fanart" }); }); if (fileInfoPaths.size() > 1) From ceef661b70a42d7a1269a96d8e35cc51443e597c Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 15 Sep 2025 20:50:02 +0200 Subject: [PATCH 06/12] Added a new setting 'artist-info-image-file-names' to specify the artist image files to lookup alongside artist.nfo files, fixes #750 --- conf/lms.conf | 9 ++++++- .../steps/ScanStepAssociateArtistImages.cpp | 26 ++++++++++++++++--- .../steps/ScanStepAssociateArtistImages.hpp | 1 + 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/conf/lms.conf b/conf/lms.conf index 231fb777..1084f5dd 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -101,10 +101,17 @@ cover-jpeg-quality = 75; # Preferred file names for covers (order is important, accept wildcards) cover-preferred-file-names = ("cover", "front", "folder", "default"); +# Image file names searched alongside the artist info file (artist.nfo) +# Note: files whose name is the artist's MBID are always searched first. You can place the MBID files anywhere in your libraries. +artist-info-image-file-names = ("thumb", "folder", "fanart"); + # File names for artist images (order is important, accept wildcards) -# Note: files whose name is the artist's MBID are always searched before the names in this list. You can place the MBID files anywhere in your libraries. +# Note: files next to artist info files are searched first. artist-image-file-names = ("artist"); +# File names for artist.nfo files +XXX = ("thumb", "folder", "fanart"); + # File names for medium images (order is important, accept wildcards) # Note: files named after the disc itself are always searched before the names in this list. medium-image-file-names = ("discsubtitle"); diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index e2fa7aea..885ad2d9 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -59,6 +59,7 @@ namespace lms::scanner struct SearchArtistArtworkParams { std::span artistFileNames; + std::span artistInfoFileNames; const ScannerSettings& settings; }; @@ -101,7 +102,7 @@ namespace lms::scanner return image; } - db::Image::pointer searchImageInArtistInfoDirectory(db::Session& session, db::ArtistId artistId) + db::Image::pointer searchImageInArtistInfoDirectory(db::Session& session, const SearchArtistArtworkParams& searchParams, db::ArtistId artistId) { db::Image::pointer image; @@ -109,9 +110,8 @@ namespace lms::scanner db::ArtistInfo::find(session, artistId, [&](const db::ArtistInfo::pointer& artistInfo) { fileInfoPaths.push_back(artistInfo->getAbsoluteFilePath()); - // TODO make these names configurable if (!image) - image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), std::array{ "thumb", "folder", "fanart" }); + image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), searchParams.artistInfoFileNames); }); if (fileInfoPaths.size() > 1) @@ -199,7 +199,7 @@ namespace lms::scanner } { - const db::Image::pointer image{ searchImageInArtistInfoDirectory(session, artist->getId()) }; + const db::Image::pointer image{ searchImageInArtistInfoDirectory(session, searchParams, artist->getId()) }; if (image) return db::Artwork::find(session, image->getId()); } @@ -254,6 +254,19 @@ namespace lms::scanner return res; } + std::vector constructArtistInfoFileNames() + { + std::vector res; + + core::Service::get()->visitStrings("artist-info-image-file-names", + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "thumb", "folder", "fanart" }); + + return res; + } + bool fetchNextArtistIdRange(db::Session& session, db::ArtistId& lastRetrievedId, db::IdRange& idRange) { constexpr std::size_t readBatchSize{ 100 }; @@ -275,6 +288,9 @@ namespace lms::scanner , _artistIdRange{ artistIdRange } { } + ~ComputeArtistArtworkAssociationsJob() override = default; + ComputeArtistArtworkAssociationsJob(const ComputeArtistArtworkAssociationsJob&) = delete; + ComputeArtistArtworkAssociationsJob& operator=(const ComputeArtistArtworkAssociationsJob&) = delete; std::span getAssociations() const { return _associations; } std::size_t getProcessedArtistCount() const { return _processedArtistCount; } @@ -315,6 +331,7 @@ namespace lms::scanner ScanStepAssociateArtistImages::ScanStepAssociateArtistImages(InitParams& initParams) : ScanStepBase{ initParams } , _artistFileNames{ constructArtistFileNames() } + , _artistInfoFileNames{ constructArtistInfoFileNames() } { } @@ -340,6 +357,7 @@ namespace lms::scanner const SearchArtistArtworkParams searchParams{ .artistFileNames = _artistFileNames, + .artistInfoFileNames = _artistInfoFileNames, .settings = _settings, }; diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.hpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.hpp index f57bcee2..3cfb1d9c 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.hpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.hpp @@ -41,5 +41,6 @@ namespace lms::scanner void process(ScanContext& context) override; const std::vector _artistFileNames; + const std::vector _artistInfoFileNames; }; } // namespace lms::scanner From 2b85de7939bf5fb050bbc13e2882c36ebc729983 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 16 Sep 2025 22:20:59 +0200 Subject: [PATCH 07/12] Removed bad entry, ref #750 --- conf/lms.conf | 3 --- 1 file changed, 3 deletions(-) diff --git a/conf/lms.conf b/conf/lms.conf index 1084f5dd..269ae960 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -109,9 +109,6 @@ artist-info-image-file-names = ("thumb", "folder", "fanart"); # Note: files next to artist info files are searched first. artist-image-file-names = ("artist"); -# File names for artist.nfo files -XXX = ("thumb", "folder", "fanart"); - # File names for medium images (order is important, accept wildcards) # Note: files named after the disc itself are always searched before the names in this list. medium-image-file-names = ("discsubtitle"); From c65b7b3e0b4725f9405321cd4214b59e86659759 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 19 Sep 2025 00:34:37 +0200 Subject: [PATCH 08/12] replaced boost by manual xml writing, to improve serialization perfs --- src/libs/core/impl/String.cpp | 22 +- src/libs/core/include/core/String.hpp | 3 + src/libs/core/test/String.cpp | 21 ++ src/libs/subsonic/CMakeLists.txt | 4 + src/libs/subsonic/impl/SubsonicResponse.cpp | 282 +++++++++--------- src/libs/subsonic/impl/SubsonicResponse.hpp | 17 +- src/libs/subsonic/test/CMakeLists.txt | 20 ++ .../subsonic/test/SubsonicResponseTest.cpp | 107 +++++++ 8 files changed, 335 insertions(+), 141 deletions(-) create mode 100644 src/libs/subsonic/test/CMakeLists.txt create mode 100644 src/libs/subsonic/test/SubsonicResponseTest.cpp diff --git a/src/libs/core/impl/String.cpp b/src/libs/core/impl/String.cpp index 64650c77..31a1e84b 100644 --- a/src/libs/core/impl/String.cpp +++ b/src/libs/core/impl/String.cpp @@ -44,10 +44,20 @@ namespace lms::core::stringUtils constexpr std::pair jsonEscapeChars[]{ { '\\', "\\\\" }, + { '"', "\\\"" }, + { '\b', "\\b" }, + { '\f', "\\f" }, { '\n', "\\n" }, { '\r', "\\r" }, { '\t', "\\t" }, - { '"', "\\\"" }, + }; + + constexpr std::pair xmlEscapeChars[]{ + { '&', "&" }, + { '<', "<" }, + { '>', ">" }, + { '\'', "'" }, + { '"', """ }, }; template @@ -442,6 +452,16 @@ namespace lms::core::stringUtils details::writeEscapedString(os, str, details::jsonEscapeChars); } + std::string xmlEscape(std::string_view str) + { + return details::escape(str, details::xmlEscapeChars); + } + + void writeXmlEscapedString(std::ostream& os, std::string_view str) + { + details::writeEscapedString(os, str, details::xmlEscapeChars); + } + std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) { std::string res; diff --git a/src/libs/core/include/core/String.hpp b/src/libs/core/include/core/String.hpp index f096c8e2..62010795 100644 --- a/src/libs/core/include/core/String.hpp +++ b/src/libs/core/include/core/String.hpp @@ -105,6 +105,9 @@ namespace lms::core::stringUtils void writeJSEscapedString(std::ostream& os, std::string_view str); void writeJsonEscapedString(std::ostream& os, std::string_view str); + [[nodiscard]] std::string xmlEscape(std::string_view str); + void writeXmlEscapedString(std::ostream& os, std::string_view str); + [[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); [[nodiscard]] std::string unescapeString(std::string_view str, char escapeChar); diff --git a/src/libs/core/test/String.cpp b/src/libs/core/test/String.cpp index 12253fd6..29b1d259 100644 --- a/src/libs/core/test/String.cpp +++ b/src/libs/core/test/String.cpp @@ -221,6 +221,27 @@ namespace lms::core::stringUtils::tests EXPECT_EQ(jsonEscape(R"(Test'.mp3)"), R"(Test'.mp3)"); EXPECT_EQ(jsonEscape(R"(Test"".mp3)"), R"(Test\"\".mp3)"); EXPECT_EQ(jsonEscape(R"(\Test\.mp3)"), R"(\\Test\\.mp3)"); + EXPECT_EQ(jsonEscape("Line1\nLine2"), R"(Line1\nLine2)"); + EXPECT_EQ(jsonEscape("Line1\rLine2"), R"(Line1\rLine2)"); + EXPECT_EQ(jsonEscape("Col1\tCol2"), R"(Col1\tCol2)"); + EXPECT_EQ(jsonEscape("Hello\bWorld"), R"(Hello\bWorld)"); + EXPECT_EQ(jsonEscape("Hello\fWorld"), R"(Hello\fWorld)"); + EXPECT_EQ(jsonEscape("Hello\nWorld"), R"(Hello\nWorld)"); + } + + TEST(StringUtils, escapeXmlString) + { + EXPECT_EQ(xmlEscape(""), ""); + EXPECT_EQ(xmlEscape("Test.mp3"), "Test.mp3"); + EXPECT_EQ(xmlEscape("A & B"), "A & B"); + EXPECT_EQ(xmlEscape(""), "<tag>"); + EXPECT_EQ(xmlEscape(R"(He said "Hello")"), "He said "Hello""); + EXPECT_EQ(xmlEscape("It's fine"), "It's fine"); + EXPECT_EQ(xmlEscape(R"(O'Hara)"), "<tag attr="val & val2">O'Hara</tag>"); + EXPECT_EQ(xmlEscape(R"(\Test\.mp3)"), R"(\Test\.mp3)"); + EXPECT_EQ(xmlEscape("Café & Tea"), "Café & Tea"); + EXPECT_EQ(xmlEscape(R"(&<>'")"), "&<>'""); + EXPECT_EQ(xmlEscape("Line1\nLine2"), "Line1\nLine2"); } TEST(StringUtils, escapeString) diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index e82ccfce..89519d64 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -64,6 +64,10 @@ target_link_libraries(lmssubsonic PUBLIC Wt::Wt ) +if(BUILD_TESTING) + add_subdirectory(test) +endif() + if (BUILD_BENCHMARKS) add_subdirectory(bench) endif() \ No newline at end of file diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 39609315..4427acb3 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -23,9 +23,8 @@ #include #include -#include - #include "core/String.hpp" +#include "core/Utils.hpp" #include "core/Version.hpp" #include "ProtocolVersion.hpp" @@ -113,133 +112,90 @@ namespace lms::api::subsonic setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch)); } - Response Response::createOkResponse(ProtocolVersion protocolVersion) + void Response::XmlSerializer::serializeNode(std::ostream& os, const Node& node, std::string_view tagName) { - return createResponseCommon(protocolVersion); - } + // Opening tag + os << '<' << tagName; - Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) - { - return createResponseCommon(protocolVersion, &error); - } - - Response Response::createResponseCommon(ProtocolVersion protocolVersion, const Error* error) - { - Response response; - Node& responseNode{ response._root.createChild("subsonic-response") }; - - responseNode.setAttribute("status", error ? "failed" : "ok"); - responseNode.setVersionAttribute(protocolVersion); - - if (error) + // Attributes + for (const auto& [key, value] : node._attributes) { - Node& errorNode{ responseNode.createChild("error") }; - errorNode.setAttribute("code", static_cast(error->getCode())); - errorNode.setAttribute("message", error->getMessage()); + os << ' ' << key.str() << '='; + os << '"'; + serializeValue(os, value); + os << '"'; } - // OpenSubsonic mandatory fields - // No big deal to send them even for legacy clients - responseNode.setAttribute("type", "lms"); - responseNode.setAttribute("serverVersion", core::getVersion()); - responseNode.setAttribute("openSubsonic", true); + // Hack + if (tagName == "subsonic-response") + os << " xmlns=\"http://subsonic.org/restapi\""; - return response; - } + bool hasChildren = !node._children.empty() || !node._childrenArrays.empty() || !node._childrenValues.empty(); + bool hasValue = node._value.has_value(); - void Response::addNode(Node::Key key, Node&& node) - { - return _root._children["subsonic-response"].addChild(key, std::move(node)); - } - - Response::Node& Response::createNode(Node::Key key) - { - return _root._children["subsonic-response"].createChild(key); - } - - Response::Node& Response::createArrayNode(Node::Key key) - { - return _root._children["subsonic-response"].createArrayChild(key); - } - - void Response::write(std::ostream& os, ResponseFormat format) const - { - switch (format) + if (!hasChildren && !hasValue) { - case ResponseFormat::xml: - writeXML(os); - break; - case ResponseFormat::json: - writeJSON(os); - break; + os << "/>"; // Self-closing tag + return; } + + os << '>'; // End opening tag + + // Node value (text content) + if (hasValue) + serializeValue(os, *node._value); + + // Child nodes + for (const auto& [key, childNode] : node._children) + serializeNode(os, childNode, key.str()); + + // Child arrays + for (const auto& [key, childArrayNodes] : node._childrenArrays) + for (const Node& childNode : childArrayNodes) + serializeNode(os, childNode, key.str()); + + // Array values + for (const auto& [key, childValues] : node._childrenValues) + { + for (const Node::ValueType& value : childValues) + { + os << '<' << key.str() << '>'; + serializeValue(os, value); + os << "'; + } + } + + // Closing tag + os << "'; + } + + void Response::XmlSerializer::serializeValue(std::ostream& os, const Node::ValueType& value) + { + std::visit(core::utils::overloads{ + [&](const Node::string& str) { core::stringUtils::writeXmlEscapedString(os, str); }, + [&](bool value) { os << (value ? "true" : "false"); }, + [&](float value) { os << value; }, + [&](long long value) { os << value; } }, + value); + } + + void Response::XmlSerializer::serializeEscapedString(std::ostream& os, std::string_view str) + { + core::stringUtils::writeXmlEscapedString(os, str); } void Response::writeXML(std::ostream& os) const { - std::function nodeToPropertyTree = [&](const Node& node) { - boost::property_tree::ptree res; + os << R"()" << '\n'; - auto valueToPropertyTree = [](const Node::ValueType& value) { - boost::property_tree::ptree res; - std::visit([&](const auto& rawValue) { - using RawValueType = std::decay_t; - if constexpr (std::is_same_v) - res.put_value(core::stringUtils::replaceInString(rawValue, "\n", "\\n")); - else - res.put_value(rawValue); - }, - value); + XmlSerializer serializer; - return res; - }; - - if (node._value) - { - res = valueToPropertyTree(*node._value); - } - else - { - for (const auto& [key, childNode] : node._children) - { - boost::property_tree::ptree& tree{ res.add_child(std::string{ key.str() }, nodeToPropertyTree(childNode)) }; - // Hardcoded attribute to simplify createOkResponse calls - if (key == "subsonic-response") - tree.put(".xmlns", "http://subsonic.org/restapi"); - } - - for (const auto& [key, childArrayNodes] : node._childrenArrays) - { - for (const Node& childNode : childArrayNodes) - res.add_child(std::string{ key.str() }, nodeToPropertyTree(childNode)); - } - - for (const auto& [key, childArrayValues] : node._childrenValues) - { - for (const Response::Node::ValueType& value : childArrayValues) - res.add_child(std::string{ key.str() }, valueToPropertyTree(value)); - } - } - - for (const auto& [key, value] : node._attributes) - { - if (std::holds_alternative(value)) - res.put("." + std::string{ key.str() }, std::get(value)); - else if (std::holds_alternative(value)) - res.put("." + std::string{ key.str() }, std::get(value)); - else if (std::holds_alternative(value)) - res.put("." + std::string{ key.str() }, std::get(value)); - else if (std::holds_alternative(value)) - res.put("." + std::string{ key.str() }, std::get(value)); - else - assert(false); - } - - return res; - }; - - const boost::property_tree::ptree root{ nodeToPropertyTree(_root) }; - boost::property_tree::write_xml(os, root); + assert(_root._children.size() == 1); + if (_root._children.size() == 1) + { + const auto& [tagName, node] = *_root._children.begin(); + serializer.serializeNode(os, node, tagName.str()); + } } void Response::JsonSerializer::serializeNode(std::ostream& os, const Response::Node& node) @@ -325,30 +281,18 @@ namespace lms::api::subsonic void Response::JsonSerializer::serializeValue(std::ostream& os, const Node::ValueType& value) { - if (std::holds_alternative(value)) - { - serializeEscapedString(os, std::get(value)); - } - else if (std::holds_alternative(value)) - { - os << (std::get(value) ? "true" : "false"); - } - else if (std::holds_alternative(value)) - { - const float d{ std::get(value) }; - if (std::isnan(d) || std::fabs(d) == std::numeric_limits::infinity()) - os << "null"; - else - os << d; - } - else if (std::holds_alternative(value)) - { - os << std::get(value); - } - else - { - assert(false); - } + std::visit( + core::utils::overloads{ + [&](const Node::string& str) { serializeEscapedString(os, str); }, + [&](bool value) { os << (value ? "true" : "false"); }, + [&](float value) { + if (std::isnan(value) || std::fabs(value) == std::numeric_limits::infinity()) + os << "null"; + else + os << value; + }, + [&](long long value) { os << value; } }, + value); } void Response::JsonSerializer::serializeEscapedString(std::ostream& os, std::string_view str) @@ -358,6 +302,68 @@ namespace lms::api::subsonic os << '\"'; } + Response Response::createOkResponse(ProtocolVersion protocolVersion) + { + return createResponseCommon(protocolVersion); + } + + Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) + { + return createResponseCommon(protocolVersion, &error); + } + + Response Response::createResponseCommon(ProtocolVersion protocolVersion, const Error* error) + { + Response response; + Node& responseNode{ response._root.createChild("subsonic-response") }; + + responseNode.setAttribute("status", error ? "failed" : "ok"); + responseNode.setVersionAttribute(protocolVersion); + + if (error) + { + Node& errorNode{ responseNode.createChild("error") }; + errorNode.setAttribute("code", static_cast(error->getCode())); + errorNode.setAttribute("message", error->getMessage()); + } + + // OpenSubsonic mandatory fields + // No big deal to send them even for legacy clients + responseNode.setAttribute("type", "lms"); + responseNode.setAttribute("serverVersion", core::getVersion()); + responseNode.setAttribute("openSubsonic", true); + + return response; + } + + void Response::addNode(Node::Key key, Node&& node) + { + return _root._children["subsonic-response"].addChild(key, std::move(node)); + } + + Response::Node& Response::createNode(Node::Key key) + { + return _root._children["subsonic-response"].createChild(key); + } + + Response::Node& Response::createArrayNode(Node::Key key) + { + return _root._children["subsonic-response"].createArrayChild(key); + } + + void Response::write(std::ostream& os, ResponseFormat format) const + { + switch (format) + { + case ResponseFormat::xml: + writeXML(os); + break; + case ResponseFormat::json: + writeJSON(os); + break; + } + } + void Response::writeJSON(std::ostream& os) const { JsonSerializer serializer; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 1eec8665..44f19387 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -57,6 +57,11 @@ namespace lms::api::subsonic Error(Code code) : _code{ code } {} + virtual ~Error() = default; + + Error(const Error&) = delete; + Error& operator=(const Error&) = delete; + virtual std::string getMessage() const = 0; Code getCode() const { return _code; } @@ -318,8 +323,16 @@ namespace lms::api::subsonic { public: void serializeNode(std::ostream& os, const Node& node); - void serializeValue(std::ostream& os, const Node::ValueType& value); - void serializeEscapedString(std::ostream&, std::string_view str); + static void serializeValue(std::ostream& os, const Node::ValueType& value); + static void serializeEscapedString(std::ostream&, std::string_view str); + }; + + class XmlSerializer + { + public: + void serializeNode(std::ostream& os, const Node& node, std::string_view tagName); + static void serializeValue(std::ostream& os, const Node::ValueType& value); + static void serializeEscapedString(std::ostream&, std::string_view str); }; void writeJSON(std::ostream& os) const; diff --git a/src/libs/subsonic/test/CMakeLists.txt b/src/libs/subsonic/test/CMakeLists.txt new file mode 100644 index 00000000..6e79cf78 --- /dev/null +++ b/src/libs/subsonic/test/CMakeLists.txt @@ -0,0 +1,20 @@ +include(GoogleTest) + +add_executable(test-subsonic + SubsonicResponseTest.cpp + ) + +target_include_directories(test-subsonic PRIVATE + ../impl + ) + +target_link_libraries(test-subsonic PRIVATE + lmscore + lmssubsonic + GTest::GTest + ) + +if (NOT CMAKE_CROSSCOMPILING) + gtest_discover_tests(test-subsonic) +endif() + diff --git a/src/libs/subsonic/test/SubsonicResponseTest.cpp b/src/libs/subsonic/test/SubsonicResponseTest.cpp new file mode 100644 index 00000000..3bfa4a65 --- /dev/null +++ b/src/libs/subsonic/test/SubsonicResponseTest.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 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 + +#include + +#include "ProtocolVersion.hpp" +#include "SubsonicResponse.hpp" + +namespace lms::api::subsonic::tests +{ + namespace + { + Response generateFakeResponse() + { + Response response{ Response::createOkResponse(defaultServerProtocolVersion) }; + + Response::Node& node{ response.createNode("MyNode") }; + node.setAttribute("Attr1", "value1"); + node.setAttribute("Attr2", "value2"); + node.setAttribute("attr3", ""); + node.setAttribute("attr4", true); + node.setAttribute("attr5", false); + node.setAttribute("attr6", 3.14159265359); + node.setAttribute("attr7", 333666); + + for (std::size_t i{}; i < 2; ++i) + { + Response::Node& childNode{ node.createArrayChild("MyArrayChild") }; + childNode.setAttribute("Attr42", i); + + node.addArrayValue("MyArray1", "value1"); + node.addArrayValue("MyArray1", "value2"); + for (std::size_t j{}; j < i; ++j) + node.addArrayValue("MyArray2", j); + } + + return response; + } + } // namespace + + TEST(SubsonicResponse, emptyJson) + { + Response response{ Response::createOkResponse(ProtocolVersion{ 1, 16, 0 }) }; + + std::ostringstream oss; + response.write(oss, ResponseFormat::json); + + EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.70.0","status":"ok","type":"lms","version":"1.16.0"}})"); + } + + TEST(SubsonicResponse, json) + { + Response response{ generateFakeResponse() }; + + std::ostringstream oss; + response.write(oss, ResponseFormat::json); + + EXPECT_EQ(oss.str(), R"({"subsonic-response":{"openSubsonic":true,"serverVersion":"v3.70.0","status":"ok","type":"lms","version":"1.16.0","MyNode":{"Attr1":"value1","Attr2":"value2","attr3":"","attr4":true,"attr5":false,"attr6":3.14159,"attr7":333666,"MyArrayChild":[{"Attr42":0},{"Attr42":1}],"MyArray1":["value1","value2","value1","value2"],"MyArray2":[0]}}})"); + } + + TEST(SubsonicResponse, emptyXml) + { + Response response{ Response::createOkResponse(ProtocolVersion{ 1, 16, 0 }) }; + + std::ostringstream oss; + response.write(oss, ResponseFormat::xml); + + EXPECT_EQ(oss.str(), R"( +)"); + } + + TEST(SubsonicResponse, xml) + { + Response response{ generateFakeResponse() }; + + std::ostringstream oss; + response.write(oss, ResponseFormat::xml); + + EXPECT_EQ(oss.str(), R"( +value1value2value1value20)"); + } + +} // namespace lms::api::subsonic::tests + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file From d108c9ffd25ceedff8bdf93b3dd5cdcba414179d Mon Sep 17 00:00:00 2001 From: Enrique Garcia Date: Sun, 21 Sep 2025 00:19:05 +0200 Subject: [PATCH 09/12] Make a distinction between between Lms.login as a verb and as a username synonim. --- approot/messages.xml | 1 + approot/messages_es.xml | 1 + approot/messages_fr.xml | 1 + approot/messages_it.xml | 1 + approot/messages_pl.xml | 1 + approot/messages_zh.xml | 1 + src/lms/ui/Auth.cpp | 2 +- 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/approot/messages.xml b/approot/messages.xml index 8ef86dac..9a4fd79f 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -188,6 +188,7 @@ Create administrator account +Log in Remember me Welcome! diff --git a/approot/messages_es.xml b/approot/messages_es.xml index f10b5bbe..b60112a2 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -187,6 +187,7 @@ Crear cuenta de administrador +Iniciar Sesión Recordarme ¡Bienvenido! diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 40a1ca5d..ed0a2b30 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -187,6 +187,7 @@ Creation du compte administrateur +Login Se souvenir de moi Bienvenue ! diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 0238697e..13e74ee0 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -187,6 +187,7 @@ Crea un account amministratore +Login Ricordami Benvenuto! diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 41792a50..2687adc6 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -204,6 +204,7 @@ Utwórz konto administratora +Login Zapamiętaj mnie Witam! diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 5125fd20..e5f4cda8 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -184,6 +184,7 @@ 新建管理员账号 +登陆 记住我 欢迎! diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index 20a2f297..00e1f8dc 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -219,7 +219,7 @@ namespace lms::ui } } - Wt::WPushButton* loginBtn{ bindNew("login-btn", Wt::WString::tr("Lms.login")) }; + Wt::WPushButton* loginBtn{ bindNew("login-btn", Wt::WString::tr("Lms.Auth.login")) }; loginBtn->clicked().connect(this, processAuth); updateView(model.get()); From 53391e37e831bf2c39211e11f2dc33f345e47070 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Sep 2025 17:27:07 +0200 Subject: [PATCH 10/12] Updated clang format, and restored dsd file handling that was actually broken --- .clang-format | 2 +- src/libs/av/impl/AudioFile.cpp | 10 +-- src/libs/core/impl/ChildProcess.cpp | 30 ++++---- src/libs/core/impl/FileResourceHandler.hpp | 2 +- src/libs/core/impl/TraceLogger.cpp | 4 +- src/libs/core/impl/http/ClientRequest.hpp | 2 +- src/libs/database/impl/Migration.cpp | 2 +- src/libs/database/impl/SqlQuery.hpp | 2 +- src/libs/image/impl/stb/RawImage.cpp | 8 +- src/libs/metadata/bench/Metadata.cpp | 74 +++++++++---------- src/libs/metadata/impl/AudioFileParser.cpp | 8 +- src/libs/metadata/impl/taglib/TagLibDefs.hpp | 2 +- .../metadata/impl/taglib/TagLibTagReader.cpp | 3 + src/libs/metadata/impl/taglib/Utils.cpp | 16 ++-- src/libs/metadata/test/TestTagReader.hpp | 4 +- .../services/artwork/impl/ArtworkService.cpp | 4 +- .../auth/impl/PasswordServiceBase.hpp | 4 +- .../services/auth/IPasswordService.hpp | 4 +- .../impl/features/FeaturesDefs.cpp | 2 +- .../impl/features/FeaturesEngine.cpp | 32 ++++---- .../impl/features/FeaturesEngine.hpp | 20 ++--- .../ConsecutiveArtists.cpp | 4 +- .../impl/scanners/AudioFileScanOperation.cpp | 6 +- .../steps/ScanStepAssociateArtistImages.cpp | 16 ++-- .../steps/ScanStepAssociateMediumImages.cpp | 8 +- .../steps/ScanStepAssociateReleaseImages.cpp | 8 +- .../steps/ScanStepCheckForRemovedFiles.cpp | 6 +- .../impl/TranscodingResourceHandler.hpp | 2 +- src/libs/som/impl/DataNormalizer.cpp | 6 +- src/libs/som/impl/Network.cpp | 12 +-- src/libs/subsonic/impl/SubsonicResource.cpp | 16 ++-- src/libs/subsonic/impl/SubsonicResponse.cpp | 2 +- src/libs/subsonic/impl/endpoints/Browsing.cpp | 8 +- src/libs/subsonic/impl/responses/Artist.cpp | 6 +- src/lms/main.cpp | 14 ++-- src/lms/ui/Auth.cpp | 10 +-- src/lms/ui/LmsApplication.cpp | 16 ++-- src/lms/ui/LmsTheme.hpp | 8 +- src/lms/ui/resource/AudioFileResource.hpp | 2 +- src/tools/metadata/LmsMetadata.cpp | 2 +- .../GeneticAlgorithm.hpp | 10 +-- .../LmsSimilarityParameters.cpp | 10 +-- 42 files changed, 205 insertions(+), 202 deletions(-) diff --git a/.clang-format b/.clang-format index bb47648f..4b57d50b 100644 --- a/.clang-format +++ b/.clang-format @@ -2,7 +2,7 @@ Language: Cpp BasedOnStyle: Microsoft Standard: c++20 AccessModifierOffset: -4 -AlignAfterOpenBracket: DontAlign +AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignOperands: AlignAfterOperator diff --git a/src/libs/av/impl/AudioFile.cpp b/src/libs/av/impl/AudioFile.cpp index 6c91c368..8c3ee0d2 100644 --- a/src/libs/av/impl/AudioFile.cpp +++ b/src/libs/av/impl/AudioFile.cpp @@ -204,11 +204,11 @@ namespace lms::av std::optional AudioFile::getBestStreamIndex() const { int res = ::av_find_best_stream(_context, - AVMEDIA_TYPE_AUDIO, - -1, // Auto - -1, // Auto - NULL, - 0); + AVMEDIA_TYPE_AUDIO, + -1, // Auto + -1, // Auto + NULL, + 0); if (res < 0) return std::nullopt; diff --git a/src/libs/core/impl/ChildProcess.cpp b/src/libs/core/impl/ChildProcess.cpp index dfcb532f..a0d64c82 100644 --- a/src/libs/core/impl/ChildProcess.cpp +++ b/src/libs/core/impl/ChildProcess.cpp @@ -202,24 +202,24 @@ namespace lms::core LMS_LOG(CHILDPROCESS, DEBUG, "Async read, bufferSize = " << bufferSize); boost::asio::async_read(_childStdout, boost::asio::buffer(data, bufferSize), - [this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred) { - LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred); + [this, callback{ std::move(callback) }](const boost::system::error_code& error, std::size_t bytesTransferred) { + LMS_LOG(CHILDPROCESS, DEBUG, "Async read cb - ec = '" << error.message() << "' (" << error.value() << "), bytesTransferred = " << bytesTransferred); - ReadResult readResult{ ReadResult::Success }; - if (error) - { - if (error != boost::asio::error::eof) - { - // forbidden to read any captured param here as the ChildProcess instance may already have been killed - return; - } + ReadResult readResult{ ReadResult::Success }; + if (error) + { + if (error != boost::asio::error::eof) + { + // forbidden to read any captured param here as the ChildProcess instance may already have been killed + return; + } - readResult = ReadResult::EndOfFile; - _finished = true; - } + readResult = ReadResult::EndOfFile; + _finished = true; + } - callback(readResult, bytesTransferred); - }); + callback(readResult, bytesTransferred); + }); } std::size_t ChildProcess::readSome(std::byte* data, std::size_t bufferSize) diff --git a/src/libs/core/impl/FileResourceHandler.hpp b/src/libs/core/impl/FileResourceHandler.hpp index 34beecc4..c37430bb 100644 --- a/src/libs/core/impl/FileResourceHandler.hpp +++ b/src/libs/core/impl/FileResourceHandler.hpp @@ -35,7 +35,7 @@ namespace lms::core private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - void abort() override{}; + void abort() override {}; static constexpr std::size_t _chunkSize{ 262'144 }; diff --git a/src/libs/core/impl/TraceLogger.cpp b/src/libs/core/impl/TraceLogger.cpp index 2b47cf51..da879779 100644 --- a/src/libs/core/impl/TraceLogger.cpp +++ b/src/libs/core/impl/TraceLogger.cpp @@ -78,9 +78,9 @@ namespace lms::core::tracing setMetadata("cpu_count", std::to_string(std::thread::hardware_concurrency())); setMetadata("build_type", #ifndef NDEBUG - "debug" + "debug" #else - "release" + "release" #endif ); } diff --git a/src/libs/core/impl/http/ClientRequest.hpp b/src/libs/core/impl/http/ClientRequest.hpp index 81b7e38a..effd1df0 100644 --- a/src/libs/core/impl/http/ClientRequest.hpp +++ b/src/libs/core/impl/http/ClientRequest.hpp @@ -43,7 +43,7 @@ namespace lms::core::http std::visit([&](const auto& parameters) { res = &static_cast(parameters); }, - _parameters); + _parameters); return *res; } diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 51b39d64..cda0492a 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -344,7 +344,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( SELECT 1, 0, s_s.media_directory, "Main" FROM scan_settings s_s WHERE id = ?)", - scanSettingsId); + scanSettingsId); // Remove the outdated column in scan_settings utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings DROP media_directory"); diff --git a/src/libs/database/impl/SqlQuery.hpp b/src/libs/database/impl/SqlQuery.hpp index 507c07af..99957673 100644 --- a/src/libs/database/impl/SqlQuery.hpp +++ b/src/libs/database/impl/SqlQuery.hpp @@ -74,7 +74,7 @@ namespace lms::db class SelectStatement { public: - SelectStatement(){}; + SelectStatement() {}; SelectStatement(const std::string& item); SelectStatement& And(const std::string& item); diff --git a/src/libs/image/impl/stb/RawImage.cpp b/src/libs/image/impl/stb/RawImage.cpp index b06d3be0..d02fb438 100644 --- a/src/libs/image/impl/stb/RawImage.cpp +++ b/src/libs/image/impl/stb/RawImage.cpp @@ -74,13 +74,13 @@ namespace lms::image::STB #if STB_IMAGE_RESIZE_VERSION == 1 if (::stbir_resize_uint8_srgb(reinterpret_cast(_data.get()), _width, _height, 0, - reinterpret_cast(resizedData.get()), width, height, 0, - 3, STBIR_ALPHA_CHANNEL_NONE, 0) + reinterpret_cast(resizedData.get()), width, height, 0, + 3, STBIR_ALPHA_CHANNEL_NONE, 0) == 0) #elif STB_IMAGE_RESIZE_VERSION == 2 if (::stbir_resize_uint8_srgb(reinterpret_cast(_data.get()), _width, _height, 0, - reinterpret_cast(resizedData.get()), width, height, 0, - STBIR_RGB) + reinterpret_cast(resizedData.get()), width, height, 0, + STBIR_RGB) == 0) #else #error "Unhandled STB image resize version"! diff --git a/src/libs/metadata/bench/Metadata.cpp b/src/libs/metadata/bench/Metadata.cpp index 306788ca..16195076 100644 --- a/src/libs/metadata/bench/Metadata.cpp +++ b/src/libs/metadata/bench/Metadata.cpp @@ -75,43 +75,43 @@ namespace lms::metadata::benchmarks params.artistTagDelimiters = { "/", ";" }; // The list itself is not important, the idea is to have some volume params.artistsToNotSplit = { "AC/DC", - "+/-", - R"(A/N【eɪ-ɛn)", - "Akron/Family", - "AM/FM", - "Ashes/Dust", - "B/B/S/", - "BLCK/MRKT/RGNS", - "Body/Gate/Head", - "Body/Head", - "Born/Dead", - "Burger/Ink", - "case/lang/veirs", - "Chicago / London Underground", - "Dakota/Dakota", - "Dark/Light", - "Decades/Failures", - "The Denison/Kimball Trio", - "D-W/L-SS", - "F/i", - "Friend / Enemy", - "GZA/Genius", - "I/O", - "I/O3", - "In/Humanity", - "Love/Lust", - "Mirror/Dash", - "Model/Actress", - "N/N", - "Neither/Neither World", - "P1/E", - "Sick/Tired", - "t/e/u/", - "tide/edit", - "V/Vm", - "White/Lichens", - "White/Light", - "Yamantaka // Sonic Titan" }; + "+/-", + R"(A/N【eɪ-ɛn)", + "Akron/Family", + "AM/FM", + "Ashes/Dust", + "B/B/S/", + "BLCK/MRKT/RGNS", + "Body/Gate/Head", + "Body/Head", + "Born/Dead", + "Burger/Ink", + "case/lang/veirs", + "Chicago / London Underground", + "Dakota/Dakota", + "Dark/Light", + "Decades/Failures", + "The Denison/Kimball Trio", + "D-W/L-SS", + "F/i", + "Friend / Enemy", + "GZA/Genius", + "I/O", + "I/O3", + "In/Humanity", + "Love/Lust", + "Mirror/Dash", + "Model/Actress", + "N/N", + "Neither/Neither World", + "P1/E", + "Sick/Tired", + "t/e/u/", + "tide/edit", + "V/Vm", + "White/Lichens", + "White/Light", + "Yamantaka // Sonic Titan" }; const TestAudioFileParser parser{ params }; for (auto _ : state) diff --git a/src/libs/metadata/impl/AudioFileParser.cpp b/src/libs/metadata/impl/AudioFileParser.cpp index 4812e734..5878e7d4 100644 --- a/src/libs/metadata/impl/AudioFileParser.cpp +++ b/src/libs/metadata/impl/AudioFileParser.cpp @@ -196,10 +196,10 @@ namespace lms::metadata } std::vector getArtists(const ITagReader& tagReader, - std::initializer_list artistTagNames, - std::initializer_list artistSortTagNames, - std::initializer_list artistMBIDTagNames, - const AudioFileParserParameters& params) + std::initializer_list artistTagNames, + std::initializer_list artistSortTagNames, + std::initializer_list artistMBIDTagNames, + const AudioFileParserParameters& params) { std::vector artistNames{ getTagValuesFirstMatchAs(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) }; if (artistNames.empty()) diff --git a/src/libs/metadata/impl/taglib/TagLibDefs.hpp b/src/libs/metadata/impl/taglib/TagLibDefs.hpp index 4f13bf3e..cbcbbf6d 100644 --- a/src/libs/metadata/impl/taglib/TagLibDefs.hpp +++ b/src/libs/metadata/impl/taglib/TagLibDefs.hpp @@ -21,7 +21,7 @@ #include -#if (TAGLIB_MAJOR_VERSION > 2) +#if (TAGLIB_MAJOR_VERSION >= 2) #define TAGLIB_HAS_DSF 1 #endif diff --git a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp b/src/libs/metadata/impl/taglib/TagLibTagReader.cpp index b192a2d0..b2fd0db8 100644 --- a/src/libs/metadata/impl/taglib/TagLibTagReader.cpp +++ b/src/libs/metadata/impl/taglib/TagLibTagReader.cpp @@ -45,6 +45,7 @@ #include #include #if TAGLIB_HAS_DSF + #include #include #endif @@ -452,6 +453,8 @@ namespace lms::metadata::taglib #if TAGLIB_HAS_DSF else if (const auto* dsfProperties{ dynamic_cast(properties) }) _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); + else if (const auto* dsfProperties{ dynamic_cast(properties) }) + _audioProperties.bitsPerSample = dsfProperties->bitsPerSample(); #endif } diff --git a/src/libs/metadata/impl/taglib/Utils.cpp b/src/libs/metadata/impl/taglib/Utils.cpp index a8320fa2..2d01b6e2 100644 --- a/src/libs/metadata/impl/taglib/Utils.cpp +++ b/src/libs/metadata/impl/taglib/Utils.cpp @@ -40,6 +40,7 @@ #include #include #if TAGLIB_HAS_DSF + #include #include #endif @@ -50,14 +51,13 @@ namespace lms::metadata::taglib::utils { std::span getSupportedExtensions() { - static const std::vector supportedExtensions - { + static const std::vector supportedExtensions{ ".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus", - ".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p", - ".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc", - ".wav", + ".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p", + ".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc", + ".wav", #if TAGLIB_HAS_DSF - ".dsf", ".dff", ".dsdiff" + ".dsf", ".dff", ".dsdiff" #endif }; @@ -201,9 +201,9 @@ namespace lms::metadata::taglib::utils else if (TagLib::RIFF::WAV::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #if TAGLIB_HAS_DSF - else if (DSF::File::isSupported(stream)) + else if (TagLib::DSF::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); - else if (DSDIFF::File::isSupported(stream)) + else if (TagLib::DSDIFF::File::isSupported(stream)) file = std::make_unique(stream, readAudioProperties, audioPropertiesStyle); #endif diff --git a/src/libs/metadata/test/TestTagReader.hpp b/src/libs/metadata/test/TestTagReader.hpp index 32f47f39..d7f6b11a 100644 --- a/src/libs/metadata/test/TestTagReader.hpp +++ b/src/libs/metadata/test/TestTagReader.hpp @@ -161,9 +161,9 @@ namespace lms::metadata::tests { TagType::TotalDiscs, { "3" } }, }) }; testTags->setExtraUserTags({ { "MY_AWESOME_TAG_A", { "MyTagValue1ForTagA", "MyTagValue2ForTagA" } }, - { "MY_AWESOME_TAG_B", { "MyTagValue1ForTagB", "MyTagValue2ForTagB" } } }); + { "MY_AWESOME_TAG_B", { "MyTagValue1ForTagB", "MyTagValue2ForTagB" } } }); testTags->setPerformersTags({ { "RoleA", { "MyPerformer1ForRoleA", "MyPerformer2ForRoleA" } }, - { "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } }); + { "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } }); testTags->setLyricsTags({ { "eng", "[00:00.00]First line\n[00:01.00]Second line" } }); return testTags; diff --git a/src/libs/services/artwork/impl/ArtworkService.cpp b/src/libs/services/artwork/impl/ArtworkService.cpp index eb380d1b..d46bdf0c 100644 --- a/src/libs/services/artwork/impl/ArtworkService.cpp +++ b/src/libs/services/artwork/impl/ArtworkService.cpp @@ -47,8 +47,8 @@ namespace lms::artwork } ArtworkService::ArtworkService(db::IDb& db, - const std::filesystem::path& defaultReleaseCoverSvgPath, - const std::filesystem::path& defaultArtistImageSvgPath) + const std::filesystem::path& defaultReleaseCoverSvgPath, + const std::filesystem::path& defaultArtistImageSvgPath) : _db{ db } , _audioFileParser{ metadata::createAudioFileParser(metadata::AudioFileParserParameters{}) } , _cache{ core::Service::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 } diff --git a/src/libs/services/auth/impl/PasswordServiceBase.hpp b/src/libs/services/auth/impl/PasswordServiceBase.hpp index 3c5be452..bd15053b 100644 --- a/src/libs/services/auth/impl/PasswordServiceBase.hpp +++ b/src/libs/services/auth/impl/PasswordServiceBase.hpp @@ -48,8 +48,8 @@ namespace lms::auth virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0; CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress, - std::string_view loginName, - std::string_view password) override; + std::string_view loginName, + std::string_view password) override; std::shared_mutex _mutex; LoginThrottler _loginThrottler; diff --git a/src/libs/services/auth/include/services/auth/IPasswordService.hpp b/src/libs/services/auth/include/services/auth/IPasswordService.hpp index 83260ca5..3e67c86a 100644 --- a/src/libs/services/auth/include/services/auth/IPasswordService.hpp +++ b/src/libs/services/auth/include/services/auth/IPasswordService.hpp @@ -53,8 +53,8 @@ namespace lms::auth db::UserId userId{}; }; virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress, - std::string_view loginName, - std::string_view password) + std::string_view loginName, + std::string_view password) = 0; virtual bool canSetPasswords() const = 0; diff --git a/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp b/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp index 5a9e6b8e..20a21d1e 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesDefs.cpp @@ -382,7 +382,7 @@ namespace lms::recommendation FeatureNames res; std::transform(std::cbegin(featureDefinitions), std::cend(featureDefinitions), - std::inserter(res, std::begin(res)), [](auto itFeature) { return itFeature.first; }); + std::inserter(res, std::begin(res)), [](auto itFeature) { return itFeature.first; }); return res; } diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp index cb3bf190..8da6c6d4 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp @@ -101,10 +101,10 @@ namespace lms::recommendation std::unordered_set featureNames; std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)), - [](const auto& itFeatureSetting) { return itFeatureSetting.first; }); + [](const auto& itFeatureSetting) { return itFeatureSetting.first; }); const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t{ 0 }, - [](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) }; + [](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) }; LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions); @@ -184,8 +184,8 @@ namespace lms::recommendation LMS_LOG(RECOMMENDATION, DEBUG, "Training network..."); network.train(samples, trainSettings.iterationCount, - progressCallback ? somProgressCallback : som::Network::ProgressCallback{}, - [this] { return _loadCancelled; }); + progressCallback ? somProgressCallback : som::Network::ProgressCallback{}, + [this] { return _loadCancelled; }); LMS_LOG(RECOMMENDATION, DEBUG, "Training network DONE"); LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks..."); @@ -242,10 +242,10 @@ namespace lms::recommendation auto transaction{ session.createReadTransaction() }; similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds), - [&](TrackId trackId) { - return !Track::exists(session, trackId); - }), - std::end(similarTrackIds)); + [&](TrackId trackId) { + return !Track::exists(session, trackId); + }), + std::end(similarTrackIds)); } return similarTrackIds; @@ -263,10 +263,10 @@ namespace lms::recommendation auto transaction{ session.createReadTransaction() }; similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds), - [&](ReleaseId releaseId) { - return !Release::exists(session, releaseId); - }), - std::end(similarReleaseIds)); + [&](ReleaseId releaseId) { + return !Release::exists(session, releaseId); + }), + std::end(similarReleaseIds)); } return similarReleaseIds; @@ -302,10 +302,10 @@ namespace lms::recommendation auto transaction{ session.createReadTransaction() }; res.erase(std::remove_if(std::begin(res), std::end(res), - [&](ArtistId artistId) { - return !Artist::exists(session, artistId); - }), - std::end(res)); + [&](ArtistId artistId) { + return !Artist::exists(session, artistId); + }), + std::end(res)); } while (res.size() > maxCount) diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp index 3633a9f2..a3a13b47 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.hpp @@ -101,9 +101,9 @@ namespace lms::recommendation template std::vector getSimilarObjects(const std::vector& ids, - const ObjectMatrix& objectMatrix, - const ObjectPositions& objectPositions, - std::size_t maxCount) const; + const ObjectMatrix& objectMatrix, + const ObjectPositions& objectPositions, + std::size_t maxCount) const; db::IDb& _db; bool _loadCancelled{}; @@ -157,9 +157,9 @@ namespace lms::recommendation template std::vector FeaturesEngine::getSimilarObjects(const std::vector& ids, - const ObjectMatrix& objectMatrix, - const ObjectPositions& objectPositions, - std::size_t maxCount) const + const ObjectMatrix& objectMatrix, + const ObjectPositions& objectPositions, + std::size_t maxCount) const { std::vector res; @@ -173,10 +173,10 @@ namespace lms::recommendation // Remove objects that are already in input or already reported closestObjectIds.erase(std::remove_if(std::begin(closestObjectIds), std::end(closestObjectIds), - [&](IdType id) { - return std::find(std::cbegin(ids), std::cend(ids), id) != std::cend(ids); - }), - std::end(closestObjectIds)); + [&](IdType id) { + return std::find(std::cbegin(ids), std::cend(ids), id) != std::cend(ids); + }), + std::end(closestObjectIds)); for (IdType id : closestObjectIds) { diff --git a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp index f5a27025..ab157221 100644 --- a/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp +++ b/src/libs/services/recommendation/impl/playlist-constraints/ConsecutiveArtists.cpp @@ -35,8 +35,8 @@ namespace lms::recommendation::PlaylistGeneratorConstraint ArtistContainer intersection; std::set_intersection(std::cbegin(artists1), std::cend(artists1), - std::cbegin(artists2), std::cend(artists2), - std::back_inserter(intersection)); + std::cbegin(artists2), std::cend(artists2), + std::back_inserter(intersection)); return intersection.size(); } diff --git a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp index 284c713e..87d8b92f 100644 --- a/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/AudioFileScanOperation.cpp @@ -591,9 +591,9 @@ namespace lms::scanner // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file auto& mediaLibraries{ getScannerSettings().mediaLibraries }; if (std::none_of(std::cbegin(mediaLibraries), std::cend(mediaLibraries), - [&](const MediaLibraryInfo& libraryInfo) { - return core::pathUtils::isPathInRootPath(getFilePath(), libraryInfo.rootDirectory, &excludeDirFileName); - })) + [&](const MediaLibraryInfo& libraryInfo) { + return core::pathUtils::isPathInRootPath(getFilePath(), libraryInfo.rootDirectory, &excludeDirFileName); + })) { continue; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index 885ad2d9..3650ed5c 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -246,10 +246,10 @@ namespace lms::scanner std::vector res; core::Service::get()->visitStrings("artist-image-file-names", - [&res](std::string_view fileName) { - res.emplace_back(fileName); - }, - { "artist" }); + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "artist" }); return res; } @@ -259,10 +259,10 @@ namespace lms::scanner std::vector res; core::Service::get()->visitStrings("artist-info-image-file-names", - [&res](std::string_view fileName) { - res.emplace_back(fileName); - }, - { "thumb", "folder", "fanart" }); + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "thumb", "folder", "fanart" }); return res; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp index 1971f844..331bf201 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateMediumImages.cpp @@ -175,10 +175,10 @@ namespace lms::scanner std::vector res; core::Service::get()->visitStrings("medium-image-file-names", - [&res](std::string_view fileName) { - res.emplace_back(fileName); - }, - { "discsubtitle" }); + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "discsubtitle" }); return res; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp index 1b7038c9..b32b1ab5 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateReleaseImages.cpp @@ -204,10 +204,10 @@ namespace lms::scanner std::vector res; core::Service::get()->visitStrings("cover-preferred-file-names", - [&res](std::string_view fileName) { - res.emplace_back(fileName); - }, - { "cover", "front", "folder", "default" }); + [&res](std::string_view fileName) { + res.emplace_back(fileName); + }, + { "cover", "front", "folder", "default" }); return res; } diff --git a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp index c45903ed..2d71bde7 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepCheckForRemovedFiles.cpp @@ -101,9 +101,9 @@ namespace lms::scanner } if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries), - [&](const MediaLibraryInfo& libraryInfo) { - return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName); - })) + [&](const MediaLibraryInfo& libraryInfo) { + return core::pathUtils::isPathInRootPath(p, libraryInfo.rootDirectory, &excludeDirFileName); + })) { LMS_LOG(DBUPDATER, DEBUG, "Removing " << p << ": out of media directory"); return false; diff --git a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp index 89f6d3ce..0674a1a7 100644 --- a/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp +++ b/src/libs/services/transcoding/impl/TranscodingResourceHandler.hpp @@ -39,7 +39,7 @@ namespace lms::transcoding private: Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; - void abort() override{}; + void abort() override {}; static constexpr std::size_t _chunkSize{ 262'144 }; std::optional _estimatedContentLength; diff --git a/src/libs/som/impl/DataNormalizer.cpp b/src/libs/som/impl/DataNormalizer.cpp index df07ac1f..ab2d82b0 100644 --- a/src/libs/som/impl/DataNormalizer.cpp +++ b/src/libs/som/impl/DataNormalizer.cpp @@ -36,9 +36,9 @@ namespace lms::som const T mean{ std::accumulate(vec.begin(), vec.end(), T{}) / size }; return std::accumulate(vec.begin(), vec.end(), T{}, - [mean, size](T accumulator, const T& val) { - return accumulator + ((val - mean) * (val - mean) / (size - 1)); - }); + [mean, size](T accumulator, const T& val) { + return accumulator + ((val - mean) * (val - mean) / (size - 1)); + }); } DataNormalizer::DataNormalizer(std::size_t inputDimCount) diff --git a/src/libs/som/impl/Network.cpp b/src/libs/som/impl/Network.cpp index aa768108..f70dbb0a 100644 --- a/src/libs/som/impl/Network.cpp +++ b/src/libs/som/impl/Network.cpp @@ -212,9 +212,9 @@ namespace lms::som for (const Position& neighbourPosition : neighboursPosition) { auto min = std::min_element(refVectorsPosition.begin(), refVectorsPosition.end(), - [this, neighbourPosition](const auto& a, const auto& b) { - return (this->getRefVectorsDistance(a, neighbourPosition) < this->getRefVectorsDistance(b, neighbourPosition)); - }); + [this, neighbourPosition](const auto& a, const auto& b) { + return (this->getRefVectorsDistance(a, neighbourPosition) < this->getRefVectorsDistance(b, neighbourPosition)); + }); InputVector::Distance distance{ getRefVectorsDistance(neighbourPosition, *min) }; if (distance > maxDistance) @@ -227,9 +227,9 @@ namespace lms::som return std::nullopt; auto min{ std::min_element(std::cbegin(neighboursInfo), std::cend(neighboursInfo), - [&](const auto& a, const auto& b) { - return a.distance < b.distance; - }) }; + [&](const auto& a, const auto& b) { + return a.distance < b.distance; + }) }; return min->position; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 966ebcf4..60731a61 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -65,10 +65,10 @@ namespace lms::api::subsonic std::unordered_map res; core::Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", - [&](std::string_view client) { - res.emplace(std::string{ client }, ProtocolVersion{ .major = 1, .minor = 12, .patch = 0 }); - }, - { "DSub" }); + [&](std::string_view client) { + res.emplace(std::string{ client }, ProtocolVersion{ .major = 1, .minor = 12, .patch = 0 }); + }, + { "DSub" }); return res; } @@ -78,10 +78,10 @@ namespace lms::api::subsonic std::unordered_set res; core::Service::get()->visitStrings("api-open-subsonic-disabled-clients", - [&](std::string_view client) { - res.emplace(std::string{ client }); - }, - { "DSub" }); + [&](std::string_view client) { + res.emplace(std::string{ client }); + }, + { "DSub" }); return res; } diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 4427acb3..cd05c007 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -176,7 +176,7 @@ namespace lms::api::subsonic [&](bool value) { os << (value ? "true" : "false"); }, [&](float value) { os << value; }, [&](long long value) { os << value; } }, - value); + value); } void Response::XmlSerializer::serializeEscapedString(std::ostream& os, std::string_view str) diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index 9f783714..5d0d9e10 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -128,8 +128,8 @@ namespace lms::api::subsonic const auto artistTracks{ Track::findIds(context.dbSession, params) }; tracks.insert(std::end(tracks), - std::begin(artistTracks.results), - std::end(artistTracks.results)); + std::begin(artistTracks.results), + std::end(artistTracks.results)); } return tracks; @@ -159,8 +159,8 @@ namespace lms::api::subsonic const auto releaseTracks{ Track::findIds(context.dbSession, params) }; tracks.insert(std::end(tracks), - std::begin(releaseTracks.results), - std::end(releaseTracks.results)); + std::begin(releaseTracks.results), + std::end(releaseTracks.results)); } return tracks; diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 53dbf7d3..08ad7cf7 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -48,9 +48,9 @@ namespace lms::api::subsonic names.resize(artists.size()); std::transform(std::cbegin(artists), std::cend(artists), std::begin(names), - [](const Artist::pointer& artist) { - return artist->getName(); - }); + [](const Artist::pointer& artist) { + return artist->getName(); + }); return core::stringUtils::joinStrings(names, ", "); } diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 2d488700..a217d6d5 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -138,7 +138,7 @@ namespace lms config.visitStrings("trusted-proxies", [&](std::string_view trustedProxy) { pt.add("server.application-settings.trusted-proxy-config.trusted-proxies.proxy", std::string{ trustedProxy }); }, - { "127.0.0.1", "::1" }); + { "127.0.0.1", "::1" }); } { @@ -360,9 +360,9 @@ namespace lms // As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user server.addEntryPoint(Wt::EntryPointType::Application, - [&](const Wt::WEnvironment& env) { - return ui::LmsInitApplication::create(env); - }); + [&](const Wt::WEnvironment& env) { + return ui::LmsInitApplication::create(env); + }); LMS_LOG(MAIN, INFO, "Starting init web server..."); server.start(); @@ -455,9 +455,9 @@ namespace lms // bind UI entry point server.addEntryPoint(Wt::EntryPointType::Application, - [&database, &appManager, uiAuthenticationBackend](const Wt::WEnvironment& env) { - return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend); - }); + [&database, &appManager, uiAuthenticationBackend](const Wt::WEnvironment& env) { + return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend); + }); proxyScannerEventsToApplication(*scannerService, server); diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index 00e1f8dc..f8cfb114 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -54,11 +54,11 @@ namespace lms::ui core::Service::get()->createAuthToken(authTokenDomain, userId, hashedAuthCookie); LmsApp->setCookie(authCookieName, - authCookie, - expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(), - "", - "", - LmsApp->environment().urlScheme() == "https"); + authCookie, + expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(), + "", + "", + LmsApp->environment().urlScheme() == "https"); } class AuthModel : public Wt::WFormModel diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 45e47fe2..271b043a 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -418,7 +418,7 @@ namespace lms::ui declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); declareJavaScriptFunction("updateActiveNav", - R"(function(current) { + R"(function(current) { const menuItems = document.querySelectorAll('.nav-item a[href]:not([href=""])'); for (const menuItem of menuItems) { if (menuItem.getAttribute("href") === current) { @@ -546,13 +546,13 @@ namespace lms::ui { _scannerEvents.scanComplete.connect([this](const scanner::ScanStats& stats) { notifyMsg(Notification::Type::Info, - Wt::WString::tr("Lms.Admin.Database.scan-complete") - .arg(static_cast(stats.getTotalFileCount())) - .arg(static_cast(stats.additions)) - .arg(static_cast(stats.updates)) - .arg(static_cast(stats.deletions)) - .arg(static_cast(stats.duplicates.size())) - .arg(static_cast(stats.errorsCount))); + Wt::WString::tr("Lms.Admin.Database.scan-complete") + .arg(static_cast(stats.getTotalFileCount())) + .arg(static_cast(stats.additions)) + .arg(static_cast(stats.updates)) + .arg(static_cast(stats.deletions)) + .arg(static_cast(stats.duplicates.size())) + .arg(static_cast(stats.errorsCount))); }); } diff --git a/src/lms/ui/LmsTheme.hpp b/src/lms/ui/LmsTheme.hpp index 59009fd7..e2ee9c55 100644 --- a/src/lms/ui/LmsTheme.hpp +++ b/src/lms/ui/LmsTheme.hpp @@ -31,15 +31,15 @@ namespace lms::ui std::string name() const override; std::string resourcesUrl() const override; std::vector styleSheets() const override; - void apply(Wt::WWidget*, Wt::WWidget*, int) const override{}; - void apply(Wt::WWidget*, Wt::DomElement&, int) const override{}; + void apply(Wt::WWidget*, Wt::WWidget*, int) const override {}; + void apply(Wt::WWidget*, Wt::DomElement&, int) const override {}; std::string disabledClass() const override { return "disabled"; } std::string activeClass() const override { return "active"; }; std::string utilityCssClass(int) const override { return ""; }; bool canStyleAnchorAsButton() const override { return true; }; void applyValidationStyle(Wt::WWidget* widget, - const Wt::WValidator::Result& validation, - Wt::WFlags flags) const override; + const Wt::WValidator::Result& validation, + Wt::WFlags flags) const override; bool canBorderBoxElement(const Wt::DomElement&) const override { return true; } }; } // namespace lms::ui diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index f9ac913f..7dc4b8ef 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -34,6 +34,6 @@ namespace lms::ui private: void handleRequest(const Wt::Http::Request& request, - Wt::Http::Response& response) override; + Wt::Http::Response& response) override; }; } // namespace lms::ui diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index b9697509..190c4746 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -390,7 +390,7 @@ int main(int argc, char* argv[]) .options(allOptions) .positional(positional) .run(), - vm); + vm); program_options::notify(vm); diff --git a/src/tools/similarity-parameters/GeneticAlgorithm.hpp b/src/tools/similarity-parameters/GeneticAlgorithm.hpp index 975c0a79..9f2f534d 100644 --- a/src/tools/similarity-parameters/GeneticAlgorithm.hpp +++ b/src/tools/similarity-parameters/GeneticAlgorithm.hpp @@ -81,7 +81,7 @@ GeneticAlgorithm::simulate(const std::vector& initialPop scoredPopulation.reserve(initialPopulation.size()); std::transform(std::cbegin(initialPopulation), std::cend(initialPopulation), std::back_inserter(scoredPopulation), - [](const Individual& individual) { return ScoredIndividual{ individual }; }); + [](const Individual& individual) { return ScoredIndividual{ individual }; }); scoreAndSortPopulation(scoredPopulation); @@ -133,10 +133,10 @@ template void GeneticAlgorithm::scoreAndSortPopulation(std::vector& scoredPopulation) { parallel_foreach(_params.nbWorkers, std::begin(scoredPopulation), std::end(scoredPopulation), - [&](ScoredIndividual& scoredIndividual) { - if (!scoredIndividual.score) - scoredIndividual.score = _params.scoreFunction(scoredIndividual.individual); - }); + [&](ScoredIndividual& scoredIndividual) { + if (!scoredIndividual.score) + scoredIndividual.score = _params.scoreFunction(scoredIndividual.individual); + }); std::sort(std::begin(scoredPopulation), std::end(scoredPopulation), [](const ScoredIndividual& a, const ScoredIndividual& b) { return a.score > b.score; }); } diff --git a/src/tools/similarity-parameters/LmsSimilarityParameters.cpp b/src/tools/similarity-parameters/LmsSimilarityParameters.cpp index 1f2d8a24..0955357b 100644 --- a/src/tools/similarity-parameters/LmsSimilarityParameters.cpp +++ b/src/tools/similarity-parameters/LmsSimilarityParameters.cpp @@ -166,7 +166,7 @@ constructFeaturesCache(db::Session& session, const FeatureSettingsMap& featureSe std::unordered_set names; std::transform(std::cbegin(featureSettings), std::cend(featureSettings), std::inserter(names, std::begin(names)), - [](const auto& itFeature) { return itFeature.first; }); + [](const auto& itFeature) { return itFeature.first; }); auto transaction{ session.createReadTransaction() }; @@ -254,8 +254,8 @@ computeTrackScore(db::Session& session, db::IdType track1Id, db::IdType track2Id std::vector commonArtistIds; std::set_intersection(std::cbegin(track1ArtistIds), std::cend(track1ArtistIds), - std::cbegin(track2ArtistIds), std::cend(track2ArtistIds), - std::back_inserter(commonArtistIds)); + std::cbegin(track2ArtistIds), std::cend(track2ArtistIds), + std::back_inserter(commonArtistIds)); score += commonArtistIds.size(); } @@ -267,8 +267,8 @@ computeTrackScore(db::Session& session, db::IdType track1Id, db::IdType track2Id std::vector commonClusterIds; std::set_intersection(std::cbegin(track1ClusterIds), std::cend(track1ClusterIds), - std::cbegin(track2ClusterIds), std::cend(track2ClusterIds), - std::back_inserter(commonClusterIds)); + std::cbegin(track2ClusterIds), std::cend(track2ClusterIds), + std::back_inserter(commonClusterIds)); score += commonClusterIds.size(); } From 77386634bb9caf80714e736a2cff4812583a1fa8 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Sep 2025 17:30:43 +0200 Subject: [PATCH 11/12] Updated instructions for Debian Trixie --- INSTALL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 621071aa..76791b73 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -17,11 +17,11 @@ ## Docker _Docker_ images are available, please see detailed instructions on https://hub.docker.com/r/epoupon/lms. ## Debian packages -_Bookworm_ packages are provided for _amd64_ architectures. +_Trixie_ packages are provided for _amd64_ architectures. As root, trust the following debian package provider and add it in your list of repositories: ```sh wget --backups=1 https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings -echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian bookworm main" > /etc/apt/sources.list.d/epoupon.list +echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian trixie main" > /etc/apt/sources.list.d/epoupon.list ``` To install or upgrade _LMS_: ```sh @@ -31,13 +31,13 @@ apt install lms The _lms_ service is started just after the package installation, run by a dedicated _lms_ system user.
Please refer to [Deployment](#deployment) for further configuration options. ## From source -__Note__: this installation process and the default values of the configuration files have been written for _Debian Bookworm_ and _Debian Trixie_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. +__Note__: this installation process and the default values of the configuration files have been written for _Debian Trixie_. Therefore, you may have to adapt commands and/or paths in order to fit to your distribution. ### Build dependencies __Notes__: * a C++20 compiler is needed * ffmpeg version 4 minimum is required ```sh -apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev libpugixml-dev libgtest-dev libarchive-dev libxxhash-dev libssl-dev +apt-get install build-essential cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag-dev libpam0g-dev libpugixml-dev libgtest-dev libarchive-dev libxxhash-dev libssl-dev ``` __Notes__: * libpam0g-dev is optional (only for using PAM authentication) From 69672a5da2d9e9f0d246cb574e842b73c85ef51d Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Sep 2025 17:32:07 +0200 Subject: [PATCH 12/12] Upgraded clang-format version to 19 --- .github/workflows/clang-format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 60f10a42..233aedc8 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -9,5 +9,5 @@ jobs: - name: Run clang-format style check uses: jidicula/clang-format-action@v4.13.0 with: - clang-format-version: '13' + clang-format-version: '19' check-path: 'src' \ No newline at end of file