Merge branch 'develop' for release v3.58.0

This commit is contained in:
emeric
2024-09-18 23:50:25 +02:00
54 changed files with 1275 additions and 326 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
if (UNIX)
if (UNIX AND NOT APPLE)
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
endif ()
+7 -4
View File
@@ -38,15 +38,18 @@ _LMS_ provides several ways to help you find the music you like:
__Note__: depending on your database size and/or your hardware, the tag-based recommendation engine may significantly slow down the user interface. You can disable it in the administration settings.
## About tags
_LMS_ relies exclusively on tags to organize your music collection.
_LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md).
### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping` and `language` tags. More tags can be added in the database administration settings, even custom tags.
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
__Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
__Note__: You can use the `lms-metadata` tool to get an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
### Multiple artists
_LMS_ works best when using the default Picard settings, where the `artist` tag contains a single display-friendly value, and the `artists` tag holds the actual artist names. This ensures a cleaner, more organized representation of artist names, when multiple artists are involved.
### Multiple album artists
_LMS_ requires the `albumartists` and `albumartistssort` tags to properly handle multiple album artists on the same album. As they are custom tags, you may need to set up your favorite tagger to add them.
While LMS can manage multiple album artists using the `albumartist` tag, it works better when using the custom `albumartists` and `albumartistssort` tags, similar to how it handles regular artist tags.
__Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
```
+1
View File
@@ -24,6 +24,7 @@ The following extra fields are implemented:
* `moods`
* `musicBrainzId`
* `originalReleaseDate`
* `recordLabels`
* `releaseTypes`
* `userRating`
* `Child` response:
+26 -13
View File
@@ -66,36 +66,49 @@ namespace lms::core
static std::mutex mutex;
std::unique_lock<std::mutex> lock{ mutex };
int pipe[2];
int pipefd[2];
int res{ pipe2(pipe, O_NONBLOCK | O_CLOEXEC) };
if (res < 0)
throw SystemException{ errno, "pipe2 failed!" };
// Use 'pipe' instead of 'pipe2', more portable
if (pipe(pipefd) < 0)
throw SystemException{ errno, "pipe failed!" };
// Manually set the O_NONBLOCK and O_CLOEXEC flags for both ends of the pipe
if (fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == -1)
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
if (fcntl(pipefd[1], F_SETFL, O_NONBLOCK) == -1)
throw SystemException{ errno, "fcntl failed to set O_NONBLOCK!" };
if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1)
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1)
throw SystemException{ errno, "fcntl failed to set FD_CLOEXEC!" };
{
#if defined(__linux__) && defined(F_SETPIPE_SZ)
{
// Just a hint here to prevent the writer from writing too many bytes ahead of the reader
constexpr std::size_t pipeSize{ 65536 * 4 };
if (fcntl(pipe[0], F_SETPIPE_SZ, pipeSize) == -1)
if (fcntl(pipefd[0], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
if (fcntl(pipe[1], F_SETPIPE_SZ, pipeSize) == -1)
if (fcntl(pipefd[1], F_SETPIPE_SZ, pipeSize) == -1)
throw SystemException{ errno, "fcntl failed!" };
#endif
}
#endif
res = fork();
int res{ fork() };
if (res == -1)
throw SystemException{ errno, "fork failed!" };
if (res == 0) // CHILD
{
close(pipe[0]);
close(pipefd[0]);
close(STDIN_FILENO);
close(STDERR_FILENO);
// Replace stdout with pipe write
if (dup2(pipe[1], STDOUT_FILENO) == -1)
if (dup2(pipefd[1], STDOUT_FILENO) == -1)
exit(-1);
std::vector<const char*> execArgs;
@@ -108,10 +121,10 @@ namespace lms::core
}
else // PARENT
{
close(pipe[1]);
close(pipefd[1]);
{
boost::system::error_code assignError;
_childStdout.assign(pipe[0], assignError);
_childStdout.assign(pipefd[0], assignError);
if (assignError)
throw SystemException{ assignError, "fork failed!" };
}
+44 -16
View File
@@ -97,6 +97,41 @@ namespace lms::core::stringUtils
return res;
}
template<typename StringType>
std::vector<std::string_view> splitString(std::string_view str, std::span<const StringType> separators)
{
std::vector<std::string_view> res;
size_t currentPos{};
while (currentPos < str.size())
{
size_t nextSeparatorPos{ std::string_view::npos };
size_t sepLen{};
for (const std::string_view sep : separators)
{
if (sep.empty())
continue;
size_t found{ str.find(sep, currentPos) };
if (found < nextSeparatorPos)
{
nextSeparatorPos = found;
sepLen = sep.size();
}
}
if (nextSeparatorPos == std::string_view::npos)
break;
res.push_back(str.substr(currentPos, nextSeparatorPos - currentPos));
currentPos = nextSeparatorPos + sepLen;
}
res.push_back(str.substr(currentPos));
return res;
}
} // namespace details
template<>
@@ -129,24 +164,17 @@ namespace lms::core::stringUtils
std::vector<std::string_view> splitString(std::string_view str, std::string_view separator)
{
std::vector<std::string_view> res;
return splitString(str, std::span(&separator, 1));
}
if (separator.empty())
return { str };
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string_view> separators)
{
return details::splitString(str, separators);
}
size_t pos{};
size_t found{ str.find(separator) };
while (found != std::string_view::npos)
{
res.push_back(str.substr(pos, found - pos));
pos = found + separator.size();
found = str.find(separator, pos);
}
res.push_back(str.substr(pos));
return res;
std::vector<std::string_view> splitString(std::string_view str, std::span<const std::string> separators)
{
return details::splitString(str, separators);
}
std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter)
+19 -6
View File
@@ -40,6 +40,8 @@ namespace lms::core::stringUtils
{
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, char separator);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separator);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::span<const std::string_view> separators);
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::span<const std::string> separators);
[[nodiscard]] std::string joinStrings(std::span<const std::string> strings, std::string_view delimiter);
[[nodiscard]] std::string joinStrings(std::span<const std::string_view> strings, std::string_view delimiter);
@@ -65,14 +67,25 @@ namespace lms::core::stringUtils
template<typename T>
[[nodiscard]] std::optional<T> readAs(std::string_view str)
{
T res;
if constexpr (std::is_enum_v<T>)
{
using UnderlyingType = std::underlying_type_t<T>;
std::optional<UnderlyingType> underlyingValue{ readAs<UnderlyingType>(str) };
if (!underlyingValue)
return std::nullopt;
std::istringstream iss{ std::string{ str } };
iss >> res;
if (iss.fail())
return std::nullopt;
return static_cast<T>(*underlyingValue);
}
else
{
T res;
std::istringstream iss{ std::string{ str } };
iss >> res;
if (iss.fail())
return std::nullopt;
return res;
return res;
}
}
template<>
+32
View File
@@ -79,6 +79,9 @@ namespace lms::core::stringUtils::tests
TestCase tests[]{
{ "", "", { "" } },
{ "//", "//", { "", "" } },
{ "//abc//", "//", { "", "abc", "" } },
{ "//abc////abc//", "//", { "", "abc", "", "abc", "" } },
{ "abc", "", { "abc" } },
{ "abc", "-", { "abc" } },
{ "abc", "b", { "a", "c" } },
@@ -99,6 +102,35 @@ namespace lms::core::stringUtils::tests
}
}
TEST(StringUtils, splitString_multiStringDelim)
{
struct TestCase
{
std::string_view input;
std::vector<std::string_view> delimiters;
std::vector<std::string_view> expectedOutput;
};
TestCase tests[]{
{ "", { "" }, { "" } },
{ "abc", { "" }, { "abc" } },
{ "abc", { "b" }, { "a", "c" } },
{ "ab/cd", { "/" }, { "ab", "cd" } },
{ "ab/cd", { "/", ";" }, { "ab", "cd" } },
{ "ab;/cd", { "/", ";" }, { "ab", "", "cd" } },
{ "ab;/;cd", { "/", ";" }, { "ab", "", "", "cd" } },
{ "ab/;cd", { "/", ";" }, { "ab", "", "cd" } },
{ "ab/;/cd", { "/", ";" }, { "ab", "", "", "cd" } },
{ "ab/cd/ef", { "/", "cd" }, { "ab", "", "", "ef" } },
};
for (const TestCase& test : tests)
{
const std::vector<std::string_view> res{ splitString(test.input, test.delimiters) };
EXPECT_EQ(res, test.expectedOutput) << "Input = '" << test.input << "'";
}
}
TEST(StringUtils, joinStrings)
{
struct TestCase
+1
View File
@@ -24,6 +24,7 @@ add_library(lmsdatabase SHARED
impl/Track.cpp
impl/TrackBookmark.cpp
impl/Types.cpp
impl/UIState.cpp
impl/User.cpp
impl/Utils.cpp
)
+51 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 64 };
static constexpr Version LMS_DATABASE_VERSION{ 67 };
}
VersionInfo::VersionInfo()
@@ -714,6 +714,53 @@ SELECT
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_backend_date_time");
}
void migrateFromV64(Session& session)
{
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "label" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_label" (
"label_id" bigint,
"release_id" bigint,
primary key ("label_id", "release_id"),
constraint "fk_release_label_key1" foreign key ("label_id") references "label" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_label_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession()->execute(R"(CREATE INDEX "release_label_label" on "release_label" ("label_id"))");
session.getDboSession()->execute(R"(CREATE INDEX "release_label_release" on "release_label" ("release_id"))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV65(Session& session)
{
session.getDboSession()->execute("ALTER TABLE release ADD is_compilation BOOLEAN NOT NULL DEFAULT(false)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV66(Session& session)
{
// New way of handling UI settings
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "ui_state" (
"id" integer primary key autoincrement,
"version" integer not null,
"item" text not null,
"value" text not null,
"user_id" bigint,
constraint "fk_ui_state_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN repeat_all");
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN radio");
session.getDboSession()->execute("ALTER TABLE user DROP COLUMN cur_playing_track_pos");
}
bool doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -754,6 +801,9 @@ SELECT
{ 61, migrateFromV61 },
{ 62, migrateFromV62 },
{ 63, migrateFromV63 },
{ 64, migrateFromV64 },
{ 65, migrateFromV65 },
{ 66, migrateFromV66 },
};
bool migrationPerformed{};
+71 -8
View File
@@ -41,6 +41,9 @@ namespace lms::db
template<typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
{
assert(params.keywords.empty() || params.name.empty());
assert(!params.directory.isValid() || !params.parentDirectory.isValid());
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " from release r") };
if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName
@@ -54,11 +57,18 @@ namespace lms::db
|| params.artist.isValid()
|| params.clusters.size() == 1
|| params.mediaLibrary.isValid()
|| params.directory.isValid())
|| params.directory.isValid()
|| params.parentDirectory.isValid())
{
query.join("track t ON t.release_id = r.id");
}
if (params.parentDirectory.isValid())
{
query.join("directory d ON t.directory_id = d.id");
query.where("d.parent_directory_id = ?").bind(params.parentDirectory);
}
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
@@ -82,6 +92,9 @@ namespace lms::db
query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) <= ?").bind(params.dateRange->end);
}
if (!params.name.empty())
query.where("r.name = ?").bind(params.name);
for (std::string_view keyword : params.keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");
@@ -216,6 +229,36 @@ namespace lms::db
}
} // namespace
Label::Label(std::string_view name)
: _name{ name }
{
// As we use the name to uniquely identoify release type, we must throw (and not truncate)
if (name.size() > _maxNameLength)
throw Exception{ "Label name is too long: " + std::string{ name } + "'" };
}
Label::pointer Label::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Label>{ new Label{ name } });
}
Label::pointer Label::find(Session& session, LabelId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.id = ?").bind(id));
}
Label::pointer Label::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > _maxNameLength)
throw Exception{ "Requeted Label name is too long: " + std::string{ name } + "'" };
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.name = ?").bind(name));
}
ReleaseType::ReleaseType(std::string_view name)
: _name{ name }
{
@@ -257,13 +300,6 @@ namespace lms::db
return session.getDboSession()->add(std::unique_ptr<Release>{ new Release{ name, MBID } });
}
std::vector<Release::pointer> Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory)
{
session.checkReadTransaction();
return utils::fetchQueryResults<Release::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r").join("track t ON t.release_id = r.id").where("r.name = ?").bind(std::string(name, 0, _maxNameLength)).where("t.absolute_file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%"));
}
Release::pointer Release::find(Session& session, const core::UUID& mbid)
{
session.checkReadTransaction();
@@ -504,11 +540,21 @@ namespace lms::db
return utils::fetchQueryResults<Release::pointer>(query);
}
void Release::clearLabels()
{
_labels.clear();
}
void Release::clearReleaseTypes()
{
_releaseTypes.clear();
}
void Release::addLabel(ObjectPtr<Label> label)
{
_labels.insert(getDboPtr(label));
}
void Release::addReleaseType(ObjectPtr<ReleaseType> releaseType)
{
_releaseTypes.insert(getDboPtr(releaseType));
@@ -537,6 +583,16 @@ namespace lms::db
return utils::fetchQueryResults<ReleaseType::pointer>(_releaseTypes.find());
}
std::vector<std::string> Release::getLabelNames() const
{
std::vector<std::string> res;
for (const auto& label : _labels)
res.push_back(std::string{ label->getName() });
return res;
}
std::vector<std::string> Release::getReleaseTypeNames() const
{
std::vector<std::string> res;
@@ -547,6 +603,13 @@ namespace lms::db
return res;
}
void Release::visitLabels(const std::function<void(const Label::pointer& label)>& _func) const
{
assert(session());
auto query{ _labels.find() };
utils::forEachQueryResult(query, _func);
}
std::chrono::milliseconds Release::getDuration() const
{
assert(session());
+16 -9
View File
@@ -44,6 +44,7 @@
#include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp"
#include "database/TransactionChecker.hpp"
#include "database/UIState.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
@@ -99,6 +100,7 @@ namespace lms::db
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Directory>("directory");
_session.mapClass<Image>("image");
_session.mapClass<Label>("label");
_session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library");
_session.mapClass<RatedArtist>("rated_artist");
@@ -116,6 +118,7 @@ namespace lms::db
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<UIState>("ui_state");
_session.mapClass<User>("user");
}
@@ -188,6 +191,7 @@ namespace lms::db
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_parent_directory_idx ON directory(parent_directory_id)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
@@ -197,6 +201,8 @@ namespace lms::db
_session.execute("CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem)");
_session.execute("CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
@@ -204,34 +210,35 @@ namespace lms::db
_session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
_session.execute("CREATE INDEX IF NOT EXISTS rated_artist_user_artist_idx ON rated_artist(user_id,artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS rated_release_user_release_idx ON rated_release(user_id,release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_year_idx ON track(release_id, year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "database/UIState.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
namespace lms::db
{
UIState::UIState(std::string_view item, ObjectPtr<User> user)
: _item{ item }
, _user{ getDboPtr(user) }
{
}
UIState::pointer UIState::create(Session& session, std::string_view item, ObjectPtr<User> user)
{
return session.getDboSession()->add(std::unique_ptr<UIState>{ new UIState{ item, user } });
}
std::size_t UIState::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM ui_state"));
}
UIState::pointer UIState::find(Session& session, UIStateId settingId)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<UIState>>("SELECT ui_s from ui_state ui_s").where("ui_s.id = ?").bind(settingId) };
return utils::fetchQuerySingleResult(query);
}
UIState::pointer UIState::find(Session& session, std::string_view item, UserId userId)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<UIState>>("SELECT ui_s from ui_state ui_s").where("ui_s.item = ?").bind(item).where("ui_s.user_id = ?").bind(userId) };
return utils::fetchQuerySingleResult(query);
}
} // namespace lms::db
+1
View File
@@ -24,6 +24,7 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/UIState.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
@@ -109,6 +109,7 @@ namespace lms::db
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
std::string_view getName() const { return _name; }
ObjectPtr<Directory> getParentDirectory() const { return _parent; }
DirectoryId getParentDirectoryId() const { return _parent.id(); }
ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; }
// setters
@@ -22,6 +22,7 @@
#include <Wt/Dbo/ptr.h>
#include <cassert>
#include <functional>
#include <string>
namespace lms::db
{
@@ -32,7 +33,7 @@ namespace lms::db
IdType() = default;
IdType(ValueType id)
: _id{ id } { assert(isValid()); }
: _id{ id } {}
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
std::string toString() const
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(LabelId)
+56 -5
View File
@@ -34,6 +34,7 @@
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/DirectoryId.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
@@ -51,6 +52,34 @@ namespace lms::db
class Track;
class User;
class Label final : public Object<Label, LabelId>
{
public:
Label() = default;
static pointer find(Session& session, LabelId id);
static pointer find(Session& session, std::string_view name);
// Accessors
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
}
private:
static constexpr std::size_t _maxNameLength{ 512 };
friend class Session;
Label(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases; // releases that match this label
};
class ReleaseType final : public Object<ReleaseType, ReleaseTypeId>
{
public:
@@ -85,7 +114,8 @@ namespace lms::db
struct FindParameters
{
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (cannot be set with keywords)
std::string name; // must match this name (cannot be set with keywords)
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
std::optional<Range> range;
Wt::WDateTime writtenAfter;
@@ -97,7 +127,8 @@ namespace lms::db
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
std::string releaseType; // If set, albums that has this release type
MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library
DirectoryId directory; // if set, tracks in this directory
DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory)
DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory)
FindParameters& setClusters(std::span<const ClusterId> _clusters)
{
@@ -109,6 +140,11 @@ namespace lms::db
keywords = _keywords;
return *this;
}
FindParameters& setName(std::string_view _name)
{
name = _name;
return *this;
}
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod)
{
sortMethod = _sortMethod;
@@ -157,6 +193,11 @@ namespace lms::db
directory = _directory;
return *this;
}
FindParameters& setParentDirectory(DirectoryId _parentDirectory)
{
parentDirectory = _parentDirectory;
return *this;
}
};
Release() = default;
@@ -165,7 +206,6 @@ namespace lms::db
static std::size_t getCount(Session& session);
static bool exists(Session& session, ReleaseId id);
static pointer find(Session& session, const core::UUID& MBID);
static std::vector<pointer> find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory);
static pointer find(Session& session, ReleaseId id);
static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function<void(const Release::pointer&)>& func, MediaLibraryId library = {});
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
@@ -199,9 +239,12 @@ namespace lms::db
std::chrono::milliseconds getDuration() const;
Wt::WDateTime getLastWritten() const;
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
bool isCompilation() const { return _isCompilation; }
std::size_t getTrackCount() const;
std::vector<ObjectPtr<ReleaseType>> getReleaseTypes() const;
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
// Setters
void setName(std::string_view name) { _name = name; }
@@ -210,7 +253,10 @@ namespace lms::db
void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void setCompilation(bool value) { _isCompilation = value; }
void clearLabels();
void clearReleaseTypes();
void addLabel(ObjectPtr<Label> releaseType);
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
// Get the artists of this release
@@ -229,7 +275,10 @@ namespace lms::db
Wt::Dbo::field(a, _groupMBID, "group_mbid");
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::field(a, _isCompilation, "is_compilation");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
}
@@ -249,9 +298,11 @@ namespace lms::db
std::string _groupMBID;
std::optional<int> _totalDisc{};
std::string _artistDisplayName;
bool _isCompilation{}; // See https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#compilation-itunes-5
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes; // Release types
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
};
} // namespace lms::db
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/Dbo.h>
#include "core/String.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/UIStateId.hpp"
#include "database/UserId.hpp"
namespace lms::db
{
class User;
class Session;
class UIState final : public Object<UIState, UIStateId>
{
public:
UIState() = default;
static std::size_t getCount(Session& session);
static pointer find(Session& session, UIStateId settingId);
static pointer find(Session& session, std::string_view item, UserId userId);
// Getters
std::string_view getValue() const { return _value; };
// Setters
void setValue(std::string_view value) { _value = value; };
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _item, "item");
Wt::Dbo::field(a, _value, "value");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
UIState(std::string_view item, ObjectPtr<User> user);
static pointer create(Session& session, std::string_view item, ObjectPtr<User> user);
std::string _item;
std::string _value;
Wt::Dbo::ptr<User> _user;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(UIStateId)
+3 -16
View File
@@ -35,6 +35,7 @@ namespace lms::db
{
class AuthToken;
class Session;
class UIState;
class User final : public Object<User, UserId>
{
@@ -104,9 +105,6 @@ namespace lms::db
void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; }
void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
@@ -121,9 +119,6 @@ namespace lms::db
bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; }
TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
@@ -147,12 +142,8 @@ namespace lms::db
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI player settings
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _uiStates, Wt::Dbo::ManyToOne, "user");
}
private:
@@ -178,12 +169,8 @@ namespace lms::db
TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat };
int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate };
// User's dynamic data (UI)
int _curPlayingTrackPos{}; // Current track position in queue
bool _repeatAll{};
bool _radio{};
Wt::Dbo::collection<Wt::Dbo::ptr<AuthToken>> _authTokens;
Wt::Dbo::collection<Wt::Dbo::ptr<UIState>> _uiStates;
};
} // namespace lms::db
+5
View File
@@ -29,6 +29,8 @@
#include "database/StarredArtist.hpp"
#include "database/StarredRelease.hpp"
#include "database/StarredTrack.hpp"
#include "database/UIState.hpp"
#include "database/User.hpp"
namespace lms::db::tests
{
@@ -337,16 +339,19 @@ VALUES
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{}));
EXPECT_FALSE(RatedArtist::find(session, RatedArtistId{}));
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
EXPECT_FALSE(Release::find(session, ReleaseId{}));
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
EXPECT_FALSE(Track::find(session, TrackId{}));
EXPECT_FALSE(TrackList::find(session, TrackListId{}));
EXPECT_FALSE(UIState::find(session, UIStateId{}));
EXPECT_FALSE(User::find(session, UserId{}));
}
}
+38 -33
View File
@@ -21,6 +21,7 @@
namespace lms::db::tests
{
using ScopedLabel = ScopedEntity<db::Label>;
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
TEST_F(DatabaseFixture, Release)
@@ -251,39 +252,6 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Release_findByNameAndPath)
{
ScopedRelease release1{ session, "MyRelease" };
ScopedRelease release2{ session, "MyRelease" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release1.get());
track1.get().modify()->setAbsoluteFilePath("/tmp/foo/foo.mp3");
track2.get().modify()->setRelease(release2.get());
track2.get().modify()->setAbsoluteFilePath("/tmp/bar/bar.mp3");
}
{
auto transaction{ session.createReadTransaction() };
{
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
}
{
const auto releases{ Release::find(session, "MyRelease", "/tmp/bar") };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release2.getId());
}
}
}
TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
{
ScopedRelease release1{ session, "MyRelease" };
@@ -744,6 +712,43 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Release_isCompilation)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(release.get()->isCompilation());
}
{
auto transaction{ session.createWriteTransaction() };
release.get().modify()->setCompilation(true);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(release.get()->isCompilation());
}
}
TEST_F(DatabaseFixture, Label)
{
{
auto transaction{ session.createReadTransaction() };
Label::pointer res{ Label::find(session, "label") };
EXPECT_EQ(res, Label::pointer{});
}
ScopedLabel label{ session, "MyLabel" };
{
auto transaction{ session.createReadTransaction() };
Label::pointer res{ Label::find(session, "MyLabel") };
EXPECT_EQ(res, label.get());
}
}
TEST_F(DatabaseFixture, ReleaseType)
{
{
+59 -43
View File
@@ -46,7 +46,7 @@ namespace lms::metadata
{
if (value.find(tagDelimiter) != std::string_view::npos)
{
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiter))
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
visitTagIfNonEmpty(splitTag);
return;
@@ -80,7 +80,7 @@ namespace lms::metadata
{
if (value.find(tagDelimiter) != std::string_view::npos)
{
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiter))
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
addTagIfNonEmpty(splitTag);
return;
@@ -125,14 +125,15 @@ namespace lms::metadata
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames,
std::span<const std::string> artistTagDelimiters)
std::span<const std::string> artistTagDelimiters,
std::span<const std::string> defaultTagDelimiters)
{
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, artistTagDelimiters) };
if (artistNames.empty())
return {};
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, artistTagDelimiters) };
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, artistTagDelimiters) };
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, defaultTagDelimiters) };
std::vector<Artist> artists;
artists.reserve(artistNames.size());
@@ -175,6 +176,48 @@ namespace lms::metadata
return performers;
}
bool strIsMatchingArtistNames(std::string_view str, std::span<const std::string_view> artistNames)
{
std::string_view::size_type currentOffset{};
for (const std::string_view artistName : artistNames)
{
std::string_view::size_type newPos{ str.find(artistName, currentOffset) };
if (newPos == std::string_view::npos)
return false;
currentOffset = newPos + artistName.size();
}
return true;
}
bool strIsContainingAny(std::string_view str, std::span<const std::string> subStrs)
{
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
}
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string> artistTag, std::span<const std::string> artistTagDelimiters)
{
if (artists.size() == 1)
return artists.front().name;
else if (artists.size() > 1)
{
std::vector<std::string_view> artistNames;
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
// Picard use case: if we manage to match all artists in the "artist" tag (considered single-valued), and if it does not contain any custom artist delimiter, we use it as the display name
// Otherwise, we reconstruct the string using a standard, hardcoded, join
if (artistTag && !strIsContainingAny(*artistTag, artistTagDelimiters) && strIsMatchingArtistNames(*artistTag, artistNames))
return *artistTag;
else
return core::stringUtils::joinStrings(artistNames, ", ");
}
return "";
}
} // namespace
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
@@ -290,49 +333,20 @@ namespace lms::metadata
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _defaultTagDelimiters);
track.labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _defaultTagDelimiters);
std::vector<std::string_view> artistDelimiters{};
track.medium = getMedium(tagReader);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _artistTagDelimiters);
auto needReconstructArtistDisplayName{ [&] {
// We consider the artist display name is put in the Artist tag (picard case)
// To please most users, if we find a custom delimiter in the Artist tag, we construct the artist display string with a "nicer" join
if (!_artistTagDelimiters.empty()
&& track.artists.size() > 1
&& getTagValuesAs<std::string>(tagReader, TagType::Artist, _artistTagDelimiters).size() > 1)
{
return true;
}
// We have (true) multiple entries in the Artist tag or nothing
else if (getTagValuesAs<std::string>(tagReader, TagType::Artist, {}).size() != 1)
{
return true;
}
return false;
} };
if (needReconstructArtistDisplayName())
{
std::vector<std::string_view> artistNames;
std::transform(std::cbegin(track.artists), std::cend(track.artists), std::back_inserter(artistNames), [](const Artist& artist) -> std::string_view { return artist.name; });
track.artistDisplayName = core::stringUtils::joinStrings(artistNames, ", ");
}
else
{
track.artistDisplayName = getTagValueAs<std::string>(tagReader, TagType::Artist).value_or("");
}
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters);
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters);
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters);
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters);
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters);
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters);
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
// If a file has date but no year, set it
@@ -385,11 +399,13 @@ namespace lms::metadata
release.emplace();
release->name = std::move(*releaseName);
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or("");
release->artistDisplayName = getTagValueAs<std::string>(tagReader, TagType::AlbumArtist).value_or(""); // TODO try to join albumartists if present
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _artistTagDelimiters);
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters);
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
+3 -2
View File
@@ -63,7 +63,9 @@ namespace lms::metadata
std::string artistDisplayName;
std::vector<Artist> artists;
std::optional<std::size_t> mediumCount;
std::vector<std::string> labels;
std::vector<std::string> releaseTypes;
bool isCompilation{};
auto operator<=>(const Release&) const = default;
};
@@ -81,7 +83,7 @@ namespace lms::metadata
bool isDefault() const
{
static Medium defaultMedium;
static const Medium defaultMedium;
return *this == defaultMedium;
}
};
@@ -106,7 +108,6 @@ namespace lms::metadata
std::vector<std::string> groupings;
std::vector<std::string> genres;
std::vector<std::string> moods;
std::vector<std::string> labels;
std::vector<std::string> languages;
Tags userExtraTags;
std::optional<int> year{};
+126 -10
View File
@@ -42,6 +42,7 @@ namespace lms::metadata
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
{ TagType::Comment, { "Comment1", "Comment2" } },
{ TagType::Compilation, { "1" } },
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
@@ -128,9 +129,6 @@ namespace lms::metadata
ASSERT_EQ(track->groupings.size(), 2);
EXPECT_EQ(track->groupings[0], "Grouping1");
EXPECT_EQ(track->groupings[1], "Grouping2");
ASSERT_EQ(track->labels.size(), 2);
EXPECT_EQ(track->labels[0], "Label1");
EXPECT_EQ(track->labels[1], "Label2");
ASSERT_EQ(track->languages.size(), 2);
EXPECT_EQ(track->languages[0], "Language1");
EXPECT_EQ(track->languages[1], "Language2");
@@ -201,6 +199,10 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artists[1].name, "MyAlbumArtist2");
EXPECT_EQ(track->medium->release->artists[1].sortName, "MyAlbumArtist2SortName");
EXPECT_EQ(track->medium->release->artists[1].mbid, core::UUID::fromString("5ed3d6b3-2aed-4a03-828c-3c4d4f7406e1"));
EXPECT_TRUE(track->medium->release->isCompilation);
ASSERT_EQ(track->medium->release->labels.size(), 2);
EXPECT_EQ(track->medium->release->labels[0], "Label1");
EXPECT_EQ(track->medium->release->labels[1], "Label2");
ASSERT_TRUE(track->medium->release->mbid.has_value());
EXPECT_EQ(track->medium->release->mbid.value(), core::UUID::fromString("3fa39992-b786-4585-a70e-85d5cc15ef69"));
EXPECT_EQ(track->medium->release->groupMBID.value(), core::UUID::fromString("5b1a5a44-8420-4426-9b86-d25dc8d04838"));
@@ -233,17 +235,25 @@ namespace lms::metadata
{
const TestTagReader testTags{
{
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
{ TagType::Genre, { "Genre1 ; Genre2" } },
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
{ TagType::Artist, { " This / is ; One Artist \\ Other Artist " } },
}
};
Parser parser;
static_cast<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; ", "/" });
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " \\ ", " / " }); // The first delimiter found will be used
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / ", " feat. " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
ASSERT_EQ(track->artists.size(), 3);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[2].name, "Artist3");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2, Artist3"); // reconstruct artist display name since a custom delimiter is hit
ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
@@ -251,10 +261,16 @@ namespace lms::metadata
EXPECT_EQ(track->languages[0], "Lang1");
EXPECT_EQ(track->languages[1], "Lang2");
EXPECT_EQ(track->languages[2], "Lang3");
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "This / is ; One Artist");
EXPECT_EQ(track->artists[1].name, "Other Artist");
EXPECT_EQ(track->artistDisplayName, "This / is ; One Artist, Other Artist"); // reconstruct artist display name since a custom delimiter is hit
// Medium
ASSERT_TRUE(track->medium.has_value());
// Release
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->name, "MyAlbum");
EXPECT_EQ(track->medium->release->artists[0].name, "AlbumArtist1");
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
}
TEST(Parser, noArtistInArtist)
@@ -271,7 +287,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "");
}
TEST(Parser, singleArtistInArtist)
TEST(Parser, singleArtistInArtists)
{
const TestTagReader testTags{
{
@@ -320,4 +336,104 @@ namespace lms::metadata
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(Parser, singleArtistInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist1" } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 1);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
}
TEST(Parser, multipleArtistsInAlbumArtist)
{
const TestTagReader testTags{
{
// nothing in AlbumArtists!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtist, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(Parser, multipleArtistsInAlbumArtists)
{
const TestTagReader testTags{
{
// nothing in AlbumArtist!
{ TagType::Album, { "MyAlbum" } },
{ TagType::AlbumArtists, { "Artist1", "Artist2" } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
ASSERT_EQ(track->medium->release->artists.size(), 2);
EXPECT_EQ(track->medium->release->artists[0].name, "Artist1");
EXPECT_EQ(track->medium->release->artists[1].name, "Artist2");
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 & Artist2" } },
{ TagType::Artists, { "Artist1", "Artist2" } },
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
}
TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
{
const TestTagReader testTags{
{
{ TagType::Artist, { "Artist1 / Artist2" } },
{ TagType::MusicBrainzArtistID, { "dd2180a2-a350-4012-b332-5d66102fa2c6" } }, // only one => no mbid will be added
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artists[0].mbid, std::nullopt);
EXPECT_EQ(track->artists[1].name, "Artist2");
EXPECT_EQ(track->artists[1].mbid, std::nullopt);
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
}
} // namespace lms::metadata
@@ -134,17 +134,25 @@ namespace lms::scanner
return artist;
}
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
{
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
}
void updateArtistIfNeeded(Artist::pointer artist, const metadata::Artist& artistInfo)
{
// Name may have been updated
if (artist->getName() != artistInfo.name)
{
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'");
artist.modify()->setName(artistInfo.name);
}
// Sortname may have been updated
// As the sort name is quite often not filled in, we update it only if already set (for now?)
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
{
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'");
artist.modify()->setSortName(*artistInfo.sortName);
}
}
@@ -206,6 +214,15 @@ namespace lms::scanner
return releaseType;
}
Label::pointer getOrCreateLabel(Session& session, std::string_view name)
{
Label::pointer label{ Label::find(session, name) };
if (!label)
label = session.create<Label>(name);
return label;
}
void updateReleaseIfNeeded(Session& session, Release::pointer release, const metadata::Release& releaseInfo)
{
if (release->getName() != releaseInfo.name)
@@ -218,51 +235,91 @@ namespace lms::scanner
release.modify()->setTotalDisc(releaseInfo.mediumCount);
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
if (release->isCompilation() != releaseInfo.isCompilation)
release.modify()->setCompilation(releaseInfo.isCompilation);
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
{
release.modify()->clearReleaseTypes();
for (std::string_view releaseType : releaseInfo.releaseTypes)
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
}
if (release->getLabelNames() != releaseInfo.labels)
{
release.modify()->clearLabels();
for (std::string_view label : releaseInfo.labels)
release.modify()->addLabel(getOrCreateLabel(session, label));
}
}
Release::pointer getOrCreateRelease(Session& session, const metadata::Release& releaseInfo, const std::filesystem::path& expectedReleaseDirectory)
Release::pointer getOrCreateRelease(Session& session, std::optional<std::size_t> mediumPosition, const metadata::Release& releaseInfo, const Directory::pointer& currentDirectory)
{
Release::pointer release;
// First try to get by MBID
// First try to get by MBID: fastest, safest
if (releaseInfo.mbid)
{
release = Release::find(session, *releaseInfo.mbid);
if (!release)
release = session.create<Release>(releaseInfo.name, releaseInfo.mbid);
updateReleaseIfNeeded(session, release, releaseInfo);
return release;
}
// Fall back on release name (collisions may occur), if and only if it is in the current directory
if (!releaseInfo.name.empty())
else if (releaseInfo.name.empty())
{
for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name, expectedReleaseDirectory))
{
// do not fallback on properly tagged releases
if (sameNamedRelease->getMBID())
continue;
release = sameNamedRelease;
break;
}
// No release found with the same name and without MBID -> creating
if (!release)
release = session.create<Release>(releaseInfo.name);
updateReleaseIfNeeded(session, release, releaseInfo);
// No release name (only mbid) -> nothing to de
return release;
}
return Release::pointer{};
// Fall back on release name (collisions may occur)
// First try in the current directory
if (!release)
{
Release::FindParameters params;
params.setDirectory(currentDirectory->getId());
params.setName(releaseInfo.name);
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
// Already found a candidate
if (release)
return;
// Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value())
return;
// TODO: add more criterias?
release = candidateRelease;
});
}
// second try in another sibling directory (case for Album/DiscX)
const DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
if (!release && mediumPosition && parentDirectoryId.isValid())
{
Release::FindParameters params;
params.setParentDirectory(parentDirectoryId);
params.setName(releaseInfo.name);
Release::find(session, params, [&](const Release::pointer& candidateRelease) {
// Already found a candidate
if (release)
return;
// Do not fallback on properly tagged releases
if (candidateRelease->getMBID().has_value())
return;
// Fallback only if the disc number of the current track is not the same
const std::vector<DiscInfo> discs{ candidateRelease->getDiscs() };
if (discs.empty() || std::any_of(discs.begin(), discs.end(), [&](const DiscInfo& discInfo) { return discInfo.position == *mediumPosition; }))
return;
release = candidateRelease;
});
}
if (!release)
release = session.create<Release>(releaseInfo.name);
updateReleaseIfNeeded(session, release, releaseInfo);
return release;
}
std::vector<Cluster::pointer> getOrCreateClusters(Session& session, const metadata::Track& track)
@@ -638,7 +695,8 @@ namespace lms::scanner
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
track.modify()->setMediaLibrary(mediaLibrary);
track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary));
Directory::pointer directory{ getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary) };
track.modify()->setDirectory(directory);
track.modify()->clearArtistLinks();
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
@@ -679,7 +737,7 @@ namespace lms::scanner
track.modify()->setScanVersion(_settings.scanVersion);
if (trackMetadata->medium && trackMetadata->medium->release)
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata->medium->release, file.parent_path()));
track.modify()->setRelease(getOrCreateRelease(dbSession, trackMetadata->medium->position, *trackMetadata->medium->release, directory));
else
track.modify()->setRelease({});
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
+1
View File
@@ -19,6 +19,7 @@ add_library(lmssubsonic SHARED
impl/responses/ItemGenre.cpp
impl/responses/Genre.cpp
impl/responses/Playlist.cpp
impl/responses/RecordLabel.cpp
impl/responses/ReplayGain.cpp
impl/responses/Song.cpp
impl/responses/User.cpp
+2 -2
View File
@@ -409,8 +409,8 @@ namespace lms::api::subsonic
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
const ClientInfo clientInfo{ getClientInfo(request) };
const db::UserId userId{ authenticateUser(request, clientInfo) };
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) };
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
bool enableDefaultCover{ _defaultCoverClients.contains(clientInfo.name) };
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
db::User::pointer user;
+10 -14
View File
@@ -36,6 +36,7 @@
#include "responses/DiscTitle.hpp"
#include "responses/ItemDate.hpp"
#include "responses/ItemGenre.hpp"
#include "responses/RecordLabel.hpp"
namespace lms::api::subsonic
{
@@ -171,22 +172,12 @@ namespace lms::api::subsonic
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate(), release->getOriginalYear()));
{
bool isCompilation{};
albumNode.createEmptyArrayValue("releaseTypes");
for (std::string_view releaseType : release->getReleaseTypeNames())
{
if (core::stringUtils::stringCaseInsensitiveEqual(releaseType, "compilation"))
isCompilation = true;
albumNode.setAttribute("isCompilation", release->isCompilation());
albumNode.addArrayValue("releaseTypes", releaseType);
}
albumNode.createEmptyArrayValue("releaseTypes");
for (std::string_view releaseType : release->getReleaseTypeNames())
albumNode.addArrayValue("releaseTypes", releaseType);
// TODO: the Compilation tag does not have the same meaning
albumNode.setAttribute("isCompilation", isCompilation);
}
// disc titles
albumNode.createEmptyArrayChild("discTitles");
for (const DiscInfo& discInfo : release->getDiscs())
{
@@ -194,6 +185,11 @@ namespace lms::api::subsonic
albumNode.addArrayChild("discTitles", createDiscTitle(discInfo));
}
albumNode.createEmptyArrayChild("recordLabels");
release->visitLabels([&](const Label::pointer& label) {
albumNode.addArrayChild("recordLabels", createRecordLabel(label));
});
return albumNode;
}
} // namespace lms::api::subsonic
+1 -1
View File
@@ -33,7 +33,7 @@ namespace lms::db
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr<db::Release>& release, bool id3, const db::ObjectPtr<db::Directory>& directory = {});
} // namespace lms::api::subsonic
+1 -1
View File
@@ -36,7 +36,7 @@ namespace lms::db
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
namespace utils
{
+1 -1
View File
@@ -30,7 +30,7 @@ namespace lms::db
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
Response::Node createGenreNode(RequestContext& context, const db::ObjectPtr<db::Cluster>& cluster);
} // namespace lms::api::subsonic
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "responses/RecordLabel.hpp"
#include "database/Release.hpp"
namespace lms::api::subsonic
{
Response::Node createRecordLabel(const db::ObjectPtr<db::Label>& label)
{
Response::Node recordLabelNode;
recordLabelNode.setAttribute("name", label->getName());
return recordLabelNode;
}
} // namespace lms::api::subsonic
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/Object.hpp"
#include "SubsonicResponse.hpp"
namespace lms::db
{
class Label;
}
namespace lms::api::subsonic
{
Response::Node createRecordLabel(const db::ObjectPtr<db::Label>& label);
}
+4 -1
View File
@@ -106,8 +106,12 @@ namespace lms::api::subsonic
trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
}
const Release::pointer release{ track->getRelease() };
if (track->hasCover())
trackResponse.setAttribute("coverArt", idToString(track->getId()));
else if (release)
trackResponse.setAttribute("coverArt", idToString(release->getId()));
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
if (!artists.empty())
@@ -121,7 +125,6 @@ namespace lms::api::subsonic
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
}
Release::pointer release{ track->getRelease() };
if (release)
{
trackResponse.setAttribute("album", release->getName());
+1 -1
View File
@@ -32,7 +32,7 @@ namespace lms::db
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
Response::Node createSongNode(RequestContext& context, const db::ObjectPtr<db::Track>& track, bool id3);
} // namespace lms::api::subsonic
+1 -1
View File
@@ -30,7 +30,7 @@ namespace lms::db
namespace lms::api::subsonic
{
class RequestContext;
struct RequestContext;
Response::Node createUserNode(RequestContext& context, const db::ObjectPtr<db::User>& user);
} // namespace lms::api::subsonic
+1
View File
@@ -11,6 +11,7 @@ add_executable(lms
ui/NotificationContainer.cpp
ui/PlayQueue.cpp
ui/SettingsView.cpp
ui/State.cpp
ui/Utils.cpp
ui/admin/InitWizardView.cpp
ui/admin/MediaLibrariesView.cpp
+41 -25
View File
@@ -207,34 +207,34 @@ namespace lms::ui
db::User::pointer LmsApplication::getUser()
{
if (!_authenticatedUser)
if (!_user)
return {};
return db::User::find(getDbSession(), _authenticatedUser->userId);
return db::User::find(getDbSession(), _user->userId);
}
db::UserId LmsApplication::getUserId()
db::UserId LmsApplication::getUserId() const
{
return _authenticatedUser->userId;
assert(_user);
return _user->userId;
}
bool LmsApplication::isUserAuthStrong() const
{
return _authenticatedUser->strongAuth;
assert(_user);
return _user->strongAuth;
}
db::UserType LmsApplication::getUserType()
db::UserType LmsApplication::getUserType() const
{
auto transaction{ getDbSession().createReadTransaction() };
return getUser()->getType();
assert(_user);
return _user->userType;
}
std::string LmsApplication::getUserLoginName()
std::string_view LmsApplication::getUserLoginName() const
{
auto transaction{ getDbSession().createReadTransaction() };
return getUser()->getLoginName();
assert(_user);
return _user->userLoginName;
}
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
@@ -244,11 +244,10 @@ namespace lms::ui
: Wt::WApplication{ env }
, _db{ db }
, _appManager{ appManager }
, _authenticatedUser{ userId ? std::make_optional<UserAuthInfo>(UserAuthInfo{ *userId, false }) : std::nullopt }
{
try
{
init();
init(userId);
}
catch (LmsApplicationException& e)
{
@@ -264,7 +263,7 @@ namespace lms::ui
LmsApplication::~LmsApplication() = default;
void LmsApplication::init()
void LmsApplication::init(std::optional<db::UserId> userId)
{
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationInit");
@@ -279,8 +278,8 @@ namespace lms::ui
// Handle Media Scanner events and other session events
enableUpdates(true);
if (_authenticatedUser)
onUserLoggedIn();
if (userId)
onUserLoggedIn(*userId, false /* strongAuth */);
else if (core::Service<auth::IPasswordService>::exists())
processPasswordAuth();
}
@@ -292,8 +291,7 @@ namespace lms::ui
if (userId)
{
LMS_LOG(UI, DEBUG, "User authenticated using Auth token!");
_authenticatedUser = { *userId, false };
onUserLoggedIn();
onUserLoggedIn(*userId, false /* strongAuth */);
return;
}
}
@@ -315,15 +313,14 @@ namespace lms::ui
{
Auth* auth{ root()->addNew<Auth>() };
auth->userLoggedIn.connect(this, [this](db::UserId userId) {
_authenticatedUser = { userId, true };
onUserLoggedIn();
onUserLoggedIn(userId, true /* strongAuth */);
});
}
}
void LmsApplication::finalize()
{
if (_authenticatedUser)
if (_user)
_appManager.unregisterApplication(*this);
preQuit().emit();
@@ -359,10 +356,12 @@ namespace lms::ui
goHomeAndQuit();
}
void LmsApplication::onUserLoggedIn()
void LmsApplication::onUserLoggedIn(db::UserId userId, bool strongAuth)
{
root()->clear();
setUserInfo(userId, strongAuth);
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent());
_appManager.registerApplication(*this);
@@ -380,6 +379,23 @@ namespace lms::ui
createHome();
}
void LmsApplication::setUserInfo(db::UserId userId, bool strongAuth)
{
auto transaction{ getDbSession().createReadTransaction() };
const db::User::pointer user{ db::User::find(getDbSession(), userId) };
if (!user)
throw core::LmsException{ "Internal error" }; // Do not put details here at it may appear on the user rendered html
assert(!_user);
_user = UserAuthInfo{
.userId = userId,
.userType = user->getType(),
.userLoginName = user->getLoginName(),
.strongAuth = strongAuth
};
}
void LmsApplication::createHome()
{
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationCreateHome");
@@ -420,7 +436,7 @@ namespace lms::ui
navbar->bindNew<Wt::WAnchor>("tracklists", Wt::WLink{ Wt::LinkType::InternalPath, "/tracklists" }, Wt::WString::tr("Lms.Explore.tracklists"));
Filters* filters{ navbar->bindNew<Filters>("filters") };
navbar->bindString("username", getUserLoginName(), Wt::TextFormat::Plain);
navbar->bindString("username", std::string{ getUserLoginName() }, Wt::TextFormat::Plain);
navbar->bindNew<Wt::WAnchor>("settings", Wt::WLink{ Wt::LinkType::InternalPath, "/settings" }, Wt::WString::tr("Lms.Settings.menu-settings"));
{
+12 -7
View File
@@ -20,6 +20,8 @@
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <Wt/WApplication.h>
@@ -63,10 +65,10 @@ namespace lms::ui
db::Session& getDbSession(); // always thread safe
db::ObjectPtr<db::User> getUser();
db::UserId getUserId();
bool isUserAuthStrong() const; // user must be logged in prior this call
db::UserType getUserType(); // user must be logged in prior this call
std::string getUserLoginName(); // user must be logged in prior this call
db::UserId getUserId() const;
bool isUserAuthStrong() const; // user must be logged in prior this call
db::UserType getUserType() const; // user must be logged in prior this call
std::string_view getUserLoginName() const; // user must be logged in prior this call
// Proxified scanner events
scanner::Events& getScannerEvents() { return _scannerEvents; }
@@ -86,18 +88,19 @@ namespace lms::ui
Wt::Signal<>& preQuit() { return _preQuit; }
private:
void init();
void init(std::optional<db::UserId> userId);
void processPasswordAuth();
void handleException(LmsApplicationException& e);
void goHomeAndQuit();
// Signal slots
void logoutUser();
void onUserLoggedIn();
void onUserLoggedIn(db::UserId userId, bool strongAuth);
void notify(const Wt::WEvent& event) override;
void finalize() override;
void setUserInfo(db::UserId userId, bool strongAuth);
void createHome();
db::Db& _db;
@@ -107,9 +110,11 @@ namespace lms::ui
struct UserAuthInfo
{
db::UserId userId;
db::UserType userType{ db::UserType::REGULAR };
std::string userLoginName;
bool strongAuth{};
};
std::optional<UserAuthInfo> _authenticatedUser;
std::optional<UserAuthInfo> _user;
std::shared_ptr<CoverResource> _coverResource;
MediaPlayer* _mediaPlayer{};
PlayQueue* _playQueue{};
+15 -36
View File
@@ -45,6 +45,7 @@
#include "LmsApplication.hpp"
#include "MediaPlayer.hpp"
#include "ModalManager.hpp"
#include "State.hpp"
#include "Utils.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "common/MandatoryValidator.hpp"
@@ -156,35 +157,21 @@ namespace lms::ui
_repeatBtn = bindNew<Wt::WCheckBox>("repeat-btn");
_repeatBtn->clicked().connect([this] {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setRepeatAll(isRepeatAllSet());
state::writeValue<bool>("player_repeat_all", isRepeatAllSet());
});
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
if (LmsApp->getUser()->isRepeatAllSet())
_repeatBtn->setCheckState(Wt::CheckState::Checked);
}
if (state::readValue<bool>("player_repeat_all").value_or(false))
_repeatBtn->setCheckState(Wt::CheckState::Checked);
_radioBtn = bindNew<Wt::WCheckBox>("radio-btn");
_radioBtn->clicked().connect([this] {
{
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setRadio(isRadioModeSet());
state::writeValue<bool>("player_radio_mode", isRadioModeSet());
}
if (isRadioModeSet())
enqueueRadioTracksIfNeeded();
});
bool isRadioModeSet{};
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
isRadioModeSet = LmsApp->getUser()->isRadioSet();
}
if (isRadioModeSet)
if (state::readValue<bool>("player_radio_mode").value_or(false))
{
_radioBtn->setCheckState(Wt::CheckState::Checked);
enqueueRadioTracksIfNeeded();
@@ -199,21 +186,15 @@ namespace lms::ui
_mediaPlayerSettingsLoaded = true;
std::size_t trackPos{};
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
trackPos = LmsApp->getUser()->getCurPlayingTrackPos();
}
const std::size_t trackPos{ state::readValue<size_t>("player_cur_playing_track_pos").value_or(0) };
loadTrack(trackPos, false);
});
LmsApp->preQuit().connect([this] {
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
if (LmsApp->getUser()->isDemo())
if (LmsApp->getUserType() == db::UserType::DEMO)
{
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
LMS_LOG(UI, DEBUG, "Removing queue (tracklist id " << _queueId.toString() << ")");
if (db::TrackList::pointer queue{ getQueue() })
queue.remove();
@@ -271,7 +252,7 @@ namespace lms::ui
db::TrackId trackId{};
std::optional<float> replayGain{};
{
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::TrackList::pointer queue{ getQueue() };
@@ -288,16 +269,14 @@ namespace lms::ui
}
_trackPos = pos;
const db::Track::pointer track{ queue->getEntry(*_trackPos)->getTrack() };
trackId = track->getId();
replayGain = getReplayGain(pos, track);
if (!LmsApp->getUser()->isDemo())
LmsApp->getUser().modify()->setCurPlayingTrackPos(pos);
}
state::writeValue<size_t>("player_cur_playing_track_pos", pos);
enqueueRadioTracksIfNeeded();
updateCurrentTrack(true);
_isTrackSelected = true;
@@ -344,7 +323,7 @@ namespace lms::ui
db::TrackList::pointer queue;
db::TrackList::pointer radioStartingTracks;
if (!LmsApp->getUser()->isDemo())
if (LmsApp->getUserType() != db::UserType::DEMO)
{
static const std::string queueName{ "__queued_tracks__" };
queue = db::TrackList::find(LmsApp->getDbSession(), queueName, db::TrackListType::Internal, LmsApp->getUserId());
+2 -4
View File
@@ -102,7 +102,7 @@ namespace lms::ui
}
addField(PasswordField);
setValidator(PasswordField, createPasswordStrengthValidator([] { return auth::PasswordValidationContext{ LmsApp->getUserLoginName(), LmsApp->getUserType() }; }));
setValidator(PasswordField, createPasswordStrengthValidator([] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; }));
addField(PasswordConfirmField);
}
@@ -547,9 +547,7 @@ namespace lms::ui
saveBtn->clicked().connect([=] {
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
if (LmsApp->getUser()->isDemo())
if (LmsApp->getUserType() == db::UserType::DEMO)
{
LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.demo-cannot-save"));
return;
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "State.hpp"
#include "database/Session.hpp"
#include "database/UIState.hpp"
#include "database/User.hpp"
#include "LmsApplication.hpp"
namespace lms::ui::state::details
{
void writeValue(std::string_view item, std::string_view value)
{
// No UI state stored for demo user
if (LmsApp->getUserType() == db::UserType::DEMO)
return;
{
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
db::UIState::pointer state{ db::UIState::find(LmsApp->getDbSession(), item, LmsApp->getUserId()) };
if (!state)
{
if (db::User::pointer user{ LmsApp->getUser() })
state = LmsApp->getDbSession().create<db::UIState>(item, user);
}
if (state)
state.modify()->setValue(value);
}
}
std::optional<std::string> readValue(std::string_view item)
{
std::optional<std::string> res;
// No UI state stored for demo user
if (LmsApp->getUserType() == db::UserType::DEMO)
return res;
{
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
const db::UIState::pointer state{ db::UIState::find(LmsApp->getDbSession(), item, LmsApp->getUserId()) };
if (state)
res = state->getValue();
}
return res;
}
void eraseValue(std::string_view item)
{
// No UI state stored for demo user
if (LmsApp->getUserType() == db::UserType::DEMO)
return;
{
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
db::UIState::pointer state{ db::UIState::find(LmsApp->getDbSession(), item, LmsApp->getUserId()) };
if (state)
state.remove();
}
}
} // namespace lms::ui::state::details
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include "core/String.hpp"
namespace lms::ui::state
{
namespace details
{
std::optional<std::string> readValue(std::string_view item);
void writeValue(std::string_view item, std::string_view value);
void eraseValue(std::string_view item);
} // namespace details
template<typename T>
void writeValue(std::string_view item, std::optional<T> value)
{
if (value.has_value())
{
if constexpr (std::is_enum_v<T>)
details::writeValue(item, std::to_string(static_cast<std::underlying_type_t<T>>(*value)));
else
details::writeValue(item, std::to_string(*value));
}
else
details::eraseValue(item);
}
template<typename T>
std::optional<T> readValue(std::string_view item)
{
if (std::optional<std::string> res{ details::readValue(item) })
return core::stringUtils::readAs<T>(*res);
return std::nullopt;
}
} // namespace lms::ui::state
+21 -8
View File
@@ -30,6 +30,7 @@
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "SortModeSelector.hpp"
#include "State.hpp"
#include "TrackArtistLinkTypeSelector.hpp"
#include "common/InfiniteScrollingContainer.hpp"
@@ -50,15 +51,27 @@ namespace lms::ui
refreshView(searEdit->text());
});
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", _defaultSortMode) };
sortModeSelector->itemSelected.connect([this](ArtistCollector::Mode sortMode) {
refreshView(sortMode);
});
{
const ArtistCollector::Mode sortMode{ state::readValue<ArtistCollector::Mode>("artists_sort_mode").value_or(_defaultSortMode) };
_artistCollector.setMode(sortMode);
TrackArtistLinkTypeSelector* linkTypeSelector{ bindNew<TrackArtistLinkTypeSelector>("link-type", _defaultLinkType) };
linkTypeSelector->itemSelected.connect([this](std::optional<TrackArtistLinkType> linkType) {
refreshView(linkType);
});
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", sortMode) };
sortModeSelector->itemSelected.connect([this](ArtistCollector::Mode newSortMode) {
state::writeValue<ArtistCollector::Mode>("artists_sort_mode", newSortMode);
refreshView(newSortMode);
});
}
{
const std::optional<TrackArtistLinkType> linkType{ state::readValue<TrackArtistLinkType>("artists_link_type") };
_artistCollector.setArtistLinkType(linkType);
TrackArtistLinkTypeSelector* linkTypeSelector{ bindNew<TrackArtistLinkTypeSelector>("link-type", linkType) };
linkTypeSelector->itemSelected.connect([this](std::optional<TrackArtistLinkType> newLinkType) {
state::writeValue<TrackArtistLinkType>("artists_link_type", newLinkType);
refreshView(newLinkType);
});
}
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
_container->onRequestElements.connect([this] {
-1
View File
@@ -54,6 +54,5 @@ namespace lms::ui
InfiniteScrollingContainer* _container{};
ArtistCollector _artistCollector;
static constexpr ArtistCollector::Mode _defaultSortMode{ ArtistCollector::Mode::Random };
static constexpr std::optional<db::TrackArtistLinkType> _defaultLinkType{ std::nullopt };
};
} // namespace lms::ui
+3 -1
View File
@@ -45,7 +45,9 @@ namespace lms::ui
{
auto* menuItem{ bindNew<Wt::WPushButton>(var, title) };
menuItem->clicked().connect([this, menuItem, title, item] {
_currentActiveItem->removeStyleClass("active");
if (_currentActiveItem)
_currentActiveItem->removeStyleClass("active");
menuItem->addStyleClass("active");
_currentActiveItem = menuItem;
_selectedItem->setText(title);
+6
View File
@@ -32,6 +32,7 @@
#include "LmsApplication.hpp"
#include "ModalManager.hpp"
#include "State.hpp"
#include "Utils.hpp"
#include "common/ValueStringModel.hpp"
@@ -115,6 +116,7 @@ namespace lms::ui
if (const db::MediaLibraryId * mediaLibraryId{ std::get_if<db::MediaLibraryId>(&value) })
{
set(*mediaLibraryId);
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", mediaLibraryId->getValue());
}
else if (const db::ClusterId * clusterId{ std::get_if<db::ClusterId>(&value) })
{
@@ -153,6 +155,9 @@ namespace lms::ui
addFilterBtn->clicked().connect(this, &Filters::showDialog);
_filters = bindNew<Wt::WContainerWidget>("clusters");
if (const std::optional<db::MediaLibraryId::ValueType> mediaLibraryId{ state::readValue<db::MediaLibraryId::ValueType>("filters_media_library_id") })
set(*mediaLibraryId);
}
void Filters::add(db::ClusterId clusterId)
@@ -208,6 +213,7 @@ namespace lms::ui
_mediaLibraryId = db::MediaLibraryId{};
_mediaLibraryFilter = nullptr;
_sigUpdated.emit();
state::writeValue<db::MediaLibraryId::ValueType>("filters_media_library_id", std::nullopt);
});
emitFilterAddedNotification();
+11 -4
View File
@@ -27,6 +27,7 @@
#include "LmsApplication.hpp"
#include "SortModeSelector.hpp"
#include "State.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "common/Template.hpp"
#include "explore/Filters.hpp"
@@ -51,10 +52,16 @@ namespace lms::ui
refreshView(searEdit->text());
});
SortModeSelector* sortMode{ bindNew<SortModeSelector>("sort-mode", _defaultMode) };
sortMode->itemSelected.connect([this](ReleaseCollector::Mode sortMode) {
refreshView(sortMode);
});
{
const ReleaseCollector::Mode sortMode{ state::readValue<ReleaseCollector::Mode>("releases_sort_mode").value_or(_defaultMode) };
_releaseCollector.setMode(sortMode);
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", sortMode) };
sortModeSelector->itemSelected.connect([this](ReleaseCollector::Mode newSortMode) {
state::writeValue<ReleaseCollector::Mode>("releases_sort_mode", newSortMode);
refreshView(newSortMode);
});
}
Wt::WPushButton* playBtn{ bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) };
playBtn->clicked().connect([this] {
+14 -8
View File
@@ -27,6 +27,7 @@
#include "DropDownMenuSelector.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp"
#include "State.hpp"
#include "Utils.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "common/Template.hpp"
@@ -42,15 +43,20 @@ namespace lms::ui
addFunction("tr", &Wt::WTemplate::Functions::tr);
addFunction("id", &Wt::WTemplate::Functions::id);
using SortModeSelector = DropDownMenuSelector<TrackLists::Mode>;
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", Wt::WString::tr("Lms.Explore.TrackLists.template.sort-mode"), _mode) };
sortModeSelector->bindItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), Mode::RecentlyModified);
sortModeSelector->bindItem("all", Wt::WString::tr("Lms.Explore.all"), Mode::All);
{
_mode = state::readValue<Mode>("tracklists_sort_mode").value_or(_defaultMode);
sortModeSelector->itemSelected.connect(this, [this](TrackLists::Mode mode) {
_mode = mode;
refreshView();
});
using SortModeSelector = DropDownMenuSelector<TrackLists::Mode>;
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", Wt::WString::tr("Lms.Explore.TrackLists.template.sort-mode"), _mode) };
sortModeSelector->bindItem("recently-modified", Wt::WString::tr("Lms.Explore.recently-modified"), Mode::RecentlyModified);
sortModeSelector->bindItem("all", Wt::WString::tr("Lms.Explore.all"), Mode::All);
sortModeSelector->itemSelected.connect(this, [this](TrackLists::Mode mode) {
state::writeValue<Mode>("tracklists_sort_mode", mode);
_mode = mode;
refreshView();
});
}
_container = bindNew<InfiniteScrollingContainer>("tracklists", Wt::WString::tr("Lms.Explore.TrackLists.template.container"));
_container->onRequestElements.connect([this] {
+2 -1
View File
@@ -57,10 +57,11 @@ namespace lms::ui
void addSome();
void addTracklist(const db::ObjectPtr<db::TrackList>& trackList);
static constexpr Mode _defaultMode{ Mode::RecentlyModified };
static constexpr std::size_t _batchSize{ 30 };
static constexpr std::size_t _maxCount{ 500 };
Mode _mode{ Mode::RecentlyModified };
Mode _mode;
Filters& _filters;
Wt::WWidget* _currentActiveItem{};
InfiniteScrollingContainer* _container{};
+11 -4
View File
@@ -28,6 +28,7 @@
#include "LmsApplication.hpp"
#include "SortModeSelector.hpp"
#include "State.hpp"
#include "common/InfiniteScrollingContainer.hpp"
#include "explore/Filters.hpp"
#include "explore/PlayQueueController.hpp"
@@ -52,10 +53,16 @@ namespace lms::ui
refreshView(searEdit->text());
});
SortModeSelector* sortMode{ bindNew<SortModeSelector>("sort-mode", _defaultMode) };
sortMode->itemSelected.connect([this](TrackCollector::Mode mode) {
refreshView(mode);
});
{
const TrackCollector::Mode sortMode{ state::readValue<TrackCollector::Mode>("tracks_sort_mode").value_or(_defaultMode) };
_trackCollector.setMode(sortMode);
SortModeSelector* sortModeSelector{ bindNew<SortModeSelector>("sort-mode", sortMode) };
sortModeSelector->itemSelected.connect([this](TrackCollector::Mode newSortMode) {
state::writeValue<TrackCollector::Mode>("tracks_sort_mode", newSortMode);
refreshView(newSortMode);
});
}
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
->clicked()
+1
View File
@@ -6,6 +6,7 @@ add_executable(lms-metadata
target_link_libraries(lms-metadata PRIVATE
lmsmetadata
lmscore
Boost::program_options
)
install(TARGETS lms-metadata DESTINATION bin)
+76 -11
View File
@@ -25,6 +25,7 @@
#include <stdlib.h>
#include <Wt/WDate.h>
#include <boost/program_options.hpp>
#include "core/StreamLogger.hpp"
#include "metadata/Exception.hpp"
@@ -63,6 +64,9 @@ namespace lms::metadata
os << " '" << release.sortName << "'";
os << std::endl;
for (std::string_view label : release.labels)
std::cout << "\tLabel: " << label << std::endl;
if (release.mbid)
os << "\tRelease MBID = " << release.mbid->getAsString() << std::endl;
@@ -75,6 +79,8 @@ namespace lms::metadata
if (!release.artistDisplayName.empty())
std::cout << "\tDisplay artist: " << release.artistDisplayName << std::endl;
std::cout << "\tIsCompilation: " << std::boolalpha << release.isCompilation << std::endl;
for (const Artist& artist : release.artists)
std::cout << "\tRelease artist: " << artist << std::endl;
@@ -180,9 +186,6 @@ namespace lms::metadata
for (std::string_view language : track->languages)
std::cout << "Language: " << language << std::endl;
for (std::string_view label : track->labels)
std::cout << "Label: " << label << std::endl;
for (const auto& [tag, values] : track->userExtraTags)
{
std::cout << "Tag: " << tag << std::endl;
@@ -228,29 +231,87 @@ namespace lms::metadata
int main(int argc, char* argv[])
{
if (argc == 1)
{
std::cerr << "Usage: <file> [<file> ...]" << std::endl;
return EXIT_FAILURE;
}
try
{
using namespace lms;
namespace program_options = boost::program_options;
program_options::options_description options{ "Options" };
// clang-format off
options.add_options()
("help,h", "Display this help message")
("tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Tag delimiters (multiple allowed)")
("artist-tag-delimiter", program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>{}, "[]"), "Artist tag delimiters (multiple allowed)");
// clang-format on
program_options::options_description hiddenOptions{ "Hidden options" };
hiddenOptions.add_options()("file", program_options::value<std::vector<std::string>>()->composing(), "file");
program_options::options_description allOptions;
allOptions.add(options).add(hiddenOptions);
program_options::positional_options_description positional;
positional.add("file", -1); // Handle remaining arguments as input files
program_options::variables_map vm;
// Parse command line arguments with positional option handling
program_options::store(program_options::command_line_parser(argc, argv)
.options(allOptions)
.positional(positional)
.run(),
vm);
program_options::notify(vm);
auto displayHelp = [&](std::ostream& os) {
os << "Usage: " << argv[0] << " [options] file..." << std::endl;
os << options << std::endl;
};
if (vm.count("help"))
{
displayHelp(std::cout);
return EXIT_SUCCESS;
}
if (!vm.count("file"))
{
std::cout << "NO INPUT FILE!" << std::endl;
displayHelp(std::cerr);
return EXIT_FAILURE;
}
const auto& inputFiles{ vm["file"].as<std::vector<std::string>>() };
const auto& tagDelimiters{ vm["tag-delimiter"].as<std::vector<std::string>>() };
const auto& artistTagDelimiters{ vm["artist-tag-delimiter"].as<std::vector<std::string>>() };
for (std::string_view tagDelimiter : tagDelimiters)
{
std::cout << "Tag delimiter: '" << tagDelimiter << "'" << std::endl;
}
for (std::string_view artistTagDelimiter : artistTagDelimiters)
{
std::cout << "Artist tag delimiter: '" << artistTagDelimiter << "'" << std::endl;
}
// log to stdout
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout, core::logging::StreamLogger::allSeverities) };
for (std::size_t i{}; i < static_cast<std::size_t>(argc - 1); ++i)
for (const std::string& inputFile : inputFiles)
{
std::filesystem::path file{ argv[i + 1] };
std::filesystem::path file{ inputFile };
std::cout << "Parsing file '" << file << "'" << std::endl;
try
{
std::cout << "Using av:" << std::endl;
auto parser{ metadata::createParser(metadata::ParserBackend::AvFormat, metadata::ParserReadStyle::Accurate) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
parse(*parser, file);
}
catch (metadata::Exception& e)
@@ -261,7 +322,11 @@ int main(int argc, char* argv[])
try
{
std::cout << "Using TagLib:" << std::endl;
auto parser{ metadata::createParser(metadata::ParserBackend::TagLib, metadata::ParserReadStyle::Accurate) };
parser->setArtistTagDelimiters(artistTagDelimiters);
parser->setDefaultTagDelimiters(tagDelimiters);
parse(*parser, file);
}
catch (metadata::Exception& e)