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/.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
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/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)
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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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/conf/lms.conf b/conf/lms.conf
index 231fb777..269ae960 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -101,8 +101,12 @@ 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 medium images (order is important, accept wildcards)
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/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/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/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);
}
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/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/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/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/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/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/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/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/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
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));
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);
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 2a3a321c..3650ed5c 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;
@@ -110,7 +111,7 @@ namespace lms::scanner
fileInfoPaths.push_back(artistInfo->getAbsoluteFilePath());
if (!image)
- image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), std::array{ "thumb", "folder" });
+ image = findImageInDirectory(session, artistInfo->getDirectory()->getAbsolutePath(), searchParams.artistInfoFileNames);
});
if (fileInfoPaths.size() > 1)
@@ -198,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());
}
@@ -245,10 +246,23 @@ 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;
+ }
+
+ 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;
}
@@ -274,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; }
@@ -314,6 +331,7 @@ namespace lms::scanner
ScanStepAssociateArtistImages::ScanStepAssociateArtistImages(InitParams& initParams)
: ScanStepBase{ initParams }
, _artistFileNames{ constructArtistFileNames() }
+ , _artistInfoFileNames{ constructArtistInfoFileNames() }
{
}
@@ -339,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
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/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/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 39609315..cd05c007 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 << "" << key.str() << '>';
+ }
+ }
+
+ // Closing tag
+ os << "" << tagName << '>';
+ }
+
+ 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/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/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
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 20a2f297..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
@@ -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());
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();
}