Merge branch 'develop' for release v3.58.0
This commit is contained in:
+1
-1
@@ -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 ()
|
||||
|
||||
|
||||
@@ -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:
|
||||
```
|
||||
|
||||
@@ -24,6 +24,7 @@ The following extra fields are implemented:
|
||||
* `moods`
|
||||
* `musicBrainzId`
|
||||
* `originalReleaseDate`
|
||||
* `recordLabels`
|
||||
* `releaseTypes`
|
||||
* `userRating`
|
||||
* `Child` response:
|
||||
|
||||
@@ -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!" };
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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{}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -36,7 +36,7 @@ namespace lms::db
|
||||
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
class RequestContext;
|
||||
struct RequestContext;
|
||||
|
||||
namespace utils
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
{
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user