Added support for work/movements, fixes #834
This commit is contained in:
+28
-21
@@ -55,36 +55,43 @@
|
|||||||
${artist class="text-decoration-none link-success"}
|
${artist class="text-decoration-none link-success"}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.entry-disc">
|
<message id="Lms.Explore.Release.template.segment-header">
|
||||||
<h4 class="d-flex align-items-center">
|
${<if-has-artwork>}
|
||||||
${<if-has-artwork>}
|
<div class="p-1">${artwork class="Lms-cursor-pointer"}</div>
|
||||||
<div class="p-1">${artwork class="Lms-cursor-pointer"}</div>
|
${</if-has-artwork>}
|
||||||
${</if-has-artwork>}
|
<div class="p-2 flex-fill">${title}</div>
|
||||||
<div class="p-2 flex-fill">${disc-title}</div>
|
<div class="p-2 d-flex">
|
||||||
<div class="p-2 d-flex">
|
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||||
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
<div class="dropdown d-inline-block">
|
||||||
<div class="dropdown d-inline-block">
|
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
||||||
${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
|
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
||||||
<ul class="dropdown-menu" aria-labelledby="${id:more-btn}">
|
<li class="d-block d-sm-none">${play class="dropdown-item"}</li>
|
||||||
<li class="d-block d-sm-none">${play class="dropdown-item"}</li>
|
<li>${play-next class="dropdown-item"}</li>
|
||||||
<li>${play-next class="dropdown-item"}</li>
|
<li>${play-last class="dropdown-item"}</li>
|
||||||
<li>${play-last class="dropdown-item"}</li>
|
<li>${play-shuffled class="dropdown-item"}</li>
|
||||||
<li>${play-shuffled class="dropdown-item"}</li>
|
</ul>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</div>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Release.template.segment-disc">
|
||||||
|
<h4>${header class="d-flex align-items-center"}</h4>
|
||||||
|
${tracks class="d-grid gap-1"}
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message id="Lms.Explore.Release.template.segment-work">
|
||||||
|
<h5 class="text-muted">${header class="d-flex align-items-center"}</h5>
|
||||||
${tracks class="d-grid gap-1 Lms-row-container"}
|
${tracks class="d-grid gap-1 Lms-row-container"}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.entry-nodisc">
|
<message id="Lms.Explore.Release.template.segment-flat">
|
||||||
${tracks class="d-grid gap-1 Lms-row-container"}
|
${tracks class="d-grid gap-1 Lms-row-container"}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.entry">
|
<message id="Lms.Explore.Release.template.track">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="p-2 text-muted text-end Lms-tracks-entry-position">
|
<div class="p-2 text-muted text-end Lms-tracks-entry-position">
|
||||||
${<if-has-track-number>}${track-number}${</if-has-track-number>}
|
${<if-has-position>}${position}${</if-has-position>}
|
||||||
</div>
|
</div>
|
||||||
<div class="row align-items-center flex-fill overflow-hidden">
|
<div class="row align-items-center flex-fill overflow-hidden">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ namespace lms::audio::ffmpeg
|
|||||||
{ TagType::Mixers, { "MIXERS" } },
|
{ TagType::Mixers, { "MIXERS" } },
|
||||||
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
||||||
{ TagType::Mood, { "MOOD" } },
|
{ TagType::Mood, { "MOOD" } },
|
||||||
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
|
{ TagType::Movement, { "MOVEMENTNAME" } },
|
||||||
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
|
{ TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } },
|
||||||
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
|
{ TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } },
|
||||||
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
||||||
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
||||||
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ namespace lms::audio::taglib
|
|||||||
{ TagType::Mixers, { "MIXERS" } },
|
{ TagType::Mixers, { "MIXERS" } },
|
||||||
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
||||||
{ TagType::Mood, { "MOOD" } },
|
{ TagType::Mood, { "MOOD" } },
|
||||||
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
|
{ TagType::Movement, { "MOVEMENTNAME" } },
|
||||||
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
|
{ TagType::MovementCount, { "MOVEMENTCOUNT", "MOVEMENTTOTAL" } },
|
||||||
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
|
{ TagType::MovementNumber, { "MOVEMENTNUMBER", "MOVEMENT" } },
|
||||||
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
||||||
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
||||||
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
||||||
|
|||||||
@@ -418,6 +418,44 @@ namespace lms::core::stringUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string toRomanNumeral(std::size_t n)
|
||||||
|
{
|
||||||
|
if (n == 0 || n > 3999)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
static constexpr struct
|
||||||
|
{
|
||||||
|
std::size_t val;
|
||||||
|
const char* sym;
|
||||||
|
} table[]{
|
||||||
|
{ 1000, "m" },
|
||||||
|
{ 900, "cm" },
|
||||||
|
{ 500, "d" },
|
||||||
|
{ 400, "cd" },
|
||||||
|
{ 100, "c" },
|
||||||
|
{ 90, "xc" },
|
||||||
|
{ 50, "l" },
|
||||||
|
{ 40, "xl" },
|
||||||
|
{ 10, "x" },
|
||||||
|
{ 9, "ix" },
|
||||||
|
{ 5, "v" },
|
||||||
|
{ 4, "iv" },
|
||||||
|
{ 1, "i" }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string res;
|
||||||
|
for (const auto& [val, sym] : table)
|
||||||
|
{
|
||||||
|
while (n >= val)
|
||||||
|
{
|
||||||
|
res += sym;
|
||||||
|
n -= val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::string replaceInString(std::string_view str, std::string_view from, std::string_view to)
|
std::string replaceInString(std::string_view str, std::string_view from, std::string_view to)
|
||||||
{
|
{
|
||||||
std::string res{ str };
|
std::string res{ str };
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ namespace lms::core::stringUtils
|
|||||||
|
|
||||||
void capitalize(std::string& str);
|
void capitalize(std::string& str);
|
||||||
|
|
||||||
|
// returns empty string if invalid input
|
||||||
|
[[nodiscard]] std::string toRomanNumeral(std::size_t n);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
@@ -412,4 +414,32 @@ namespace lms::core::stringUtils::tests
|
|||||||
EXPECT_EQ(stringFromHex("3132333435"), "12345");
|
EXPECT_EQ(stringFromHex("3132333435"), "12345");
|
||||||
EXPECT_EQ(stringFromHex("54657374"), "Test");
|
EXPECT_EQ(stringFromHex("54657374"), "Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(StringUtils, toRomanNumeral)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(toRomanNumeral(1), "i");
|
||||||
|
EXPECT_EQ(toRomanNumeral(2), "ii");
|
||||||
|
EXPECT_EQ(toRomanNumeral(3), "iii");
|
||||||
|
EXPECT_EQ(toRomanNumeral(4), "iv");
|
||||||
|
EXPECT_EQ(toRomanNumeral(5), "v");
|
||||||
|
EXPECT_EQ(toRomanNumeral(6), "vi");
|
||||||
|
EXPECT_EQ(toRomanNumeral(7), "vii");
|
||||||
|
EXPECT_EQ(toRomanNumeral(8), "viii");
|
||||||
|
EXPECT_EQ(toRomanNumeral(9), "ix");
|
||||||
|
EXPECT_EQ(toRomanNumeral(10), "x");
|
||||||
|
EXPECT_EQ(toRomanNumeral(11), "xi");
|
||||||
|
EXPECT_EQ(toRomanNumeral(14), "xiv");
|
||||||
|
EXPECT_EQ(toRomanNumeral(16), "xvi");
|
||||||
|
EXPECT_EQ(toRomanNumeral(40), "xl");
|
||||||
|
EXPECT_EQ(toRomanNumeral(50), "l");
|
||||||
|
EXPECT_EQ(toRomanNumeral(90), "xc");
|
||||||
|
EXPECT_EQ(toRomanNumeral(99), "xcix");
|
||||||
|
EXPECT_EQ(toRomanNumeral(444), "cdxliv");
|
||||||
|
EXPECT_EQ(toRomanNumeral(1994), "mcmxciv");
|
||||||
|
EXPECT_EQ(toRomanNumeral(3999), "mmmcmxcix");
|
||||||
|
EXPECT_EQ(toRomanNumeral(0), "");
|
||||||
|
EXPECT_EQ(toRomanNumeral(4000), "");
|
||||||
|
EXPECT_EQ(toRomanNumeral(static_cast<std::size_t>(-1)), ""); // underflows to SIZE_MAX
|
||||||
|
EXPECT_EQ(toRomanNumeral(std::numeric_limits<std::size_t>::max()), "");
|
||||||
|
}
|
||||||
} // namespace lms::core::stringUtils::tests
|
} // namespace lms::core::stringUtils::tests
|
||||||
@@ -27,8 +27,10 @@ add_library(lmsdatabase STATIC
|
|||||||
impl/objects/StarredArtist.cpp
|
impl/objects/StarredArtist.cpp
|
||||||
impl/objects/StarredRelease.cpp
|
impl/objects/StarredRelease.cpp
|
||||||
impl/objects/StarredTrack.cpp
|
impl/objects/StarredTrack.cpp
|
||||||
|
impl/objects/Movement.cpp
|
||||||
impl/objects/Track.cpp
|
impl/objects/Track.cpp
|
||||||
impl/objects/TrackArtistLink.cpp
|
impl/objects/TrackArtistLink.cpp
|
||||||
|
impl/objects/Work.cpp
|
||||||
impl/objects/TrackBookmark.cpp
|
impl/objects/TrackBookmark.cpp
|
||||||
impl/objects/TrackEmbeddedImage.cpp
|
impl/objects/TrackEmbeddedImage.cpp
|
||||||
impl/objects/TrackEmbeddedImageLink.cpp
|
impl/objects/TrackEmbeddedImageLink.cpp
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 108 };
|
static constexpr Version LMS_DATABASE_VERSION{ 109 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -1922,6 +1922,40 @@ WHERE ct.name = 'GROUPING')");
|
|||||||
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster_type WHERE name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING'))");
|
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM cluster_type WHERE name IN ('GENRE', 'MOOD', 'LANGUAGE', 'GROUPING'))");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV108(Session& session)
|
||||||
|
{
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "work" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"name" text not null,
|
||||||
|
"mbid" blob
|
||||||
|
))");
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "track_work" (
|
||||||
|
"work_id" bigint,
|
||||||
|
"track_id" bigint,
|
||||||
|
primary key ("work_id", "track_id"),
|
||||||
|
constraint "fk_track_work_key1" foreign key ("work_id") references "work" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_track_work_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_work_work" on "track_work" ("work_id"))");
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE INDEX "track_work_track" on "track_work" ("track_id"))");
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(
|
||||||
|
CREATE TABLE IF NOT EXISTS "track_movement" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"name" text not null,
|
||||||
|
"number" integer,
|
||||||
|
"count" integer,
|
||||||
|
"track_id" bigint,
|
||||||
|
constraint "fk_track_movement_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred
|
||||||
|
))");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scan rescan all audio files
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -2006,6 +2040,7 @@ WHERE ct.name = 'GROUPING')");
|
|||||||
{ 105, migrateFromV105 },
|
{ 105, migrateFromV105 },
|
||||||
{ 106, migrateFromV106 },
|
{ 106, migrateFromV106 },
|
||||||
{ 107, migrateFromV107 },
|
{ 107, migrateFromV107 },
|
||||||
|
{ 108, migrateFromV108 },
|
||||||
};
|
};
|
||||||
|
|
||||||
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
|
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/PlayListFile.hpp"
|
#include "database/objects/PlayListFile.hpp"
|
||||||
#include "database/objects/PlayQueue.hpp"
|
#include "database/objects/PlayQueue.hpp"
|
||||||
#include "database/objects/Podcast.hpp"
|
#include "database/objects/Podcast.hpp"
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||||
#include "database/objects/UIState.hpp"
|
#include "database/objects/UIState.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Db.hpp"
|
#include "Db.hpp"
|
||||||
#include "Migration.hpp"
|
#include "Migration.hpp"
|
||||||
@@ -111,6 +113,7 @@ namespace lms::db
|
|||||||
_session.mapClass<StarredRelease>("starred_release");
|
_session.mapClass<StarredRelease>("starred_release");
|
||||||
_session.mapClass<StarredTrack>("starred_track");
|
_session.mapClass<StarredTrack>("starred_track");
|
||||||
_session.mapClass<Track>("track");
|
_session.mapClass<Track>("track");
|
||||||
|
_session.mapClass<Movement>("track_movement");
|
||||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||||
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
|
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
|
||||||
@@ -120,6 +123,7 @@ namespace lms::db
|
|||||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||||
_session.mapClass<TrackLyrics>("track_lyrics");
|
_session.mapClass<TrackLyrics>("track_lyrics");
|
||||||
_session.mapClass<UIState>("ui_state");
|
_session.mapClass<UIState>("ui_state");
|
||||||
|
_session.mapClass<Work>("work");
|
||||||
_session.mapClass<User>("user");
|
_session.mapClass<User>("user");
|
||||||
_session.mapClass<VersionInfo>("version_info");
|
_session.mapClass<VersionInfo>("version_info");
|
||||||
}
|
}
|
||||||
@@ -322,6 +326,10 @@ namespace lms::db
|
|||||||
"CREATE INDEX IF NOT EXISTS track_lyrics_directory_idx ON track_lyrics(directory_id)",
|
"CREATE INDEX IF NOT EXISTS track_lyrics_directory_idx ON track_lyrics(directory_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)",
|
"CREATE INDEX IF NOT EXISTS track_lyrics_track_idx ON track_lyrics(track_id)",
|
||||||
|
|
||||||
|
"CREATE INDEX IF NOT EXISTS track_movement_track_idx ON track_movement(track_id)",
|
||||||
|
|
||||||
|
"CREATE INDEX IF NOT EXISTS work_mbid_idx ON work(mbid)",
|
||||||
|
|
||||||
"CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)",
|
"CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)",
|
"CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)",
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,14 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -33,12 +33,14 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -33,12 +33,14 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -33,12 +33,14 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -31,12 +31,14 @@
|
|||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -33,12 +33,14 @@
|
|||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Impl.h>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/objects/Artist.hpp"
|
||||||
|
#include "database/objects/Artwork.hpp"
|
||||||
|
#include "database/objects/Cluster.hpp"
|
||||||
|
#include "database/objects/Directory.hpp"
|
||||||
|
#include "database/objects/Genre.hpp"
|
||||||
|
#include "database/objects/Grouping.hpp"
|
||||||
|
#include "database/objects/Language.hpp"
|
||||||
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
|
#include "database/objects/Medium.hpp"
|
||||||
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Release.hpp"
|
||||||
|
#include "database/objects/Track.hpp"
|
||||||
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
|
||||||
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|
||||||
|
DBO_INSTANTIATE_TEMPLATES(lms::db::Movement)
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
Movement::Movement(std::string_view name, std::optional<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track)
|
||||||
|
: _name{ name.substr(0, maxNameLength) }
|
||||||
|
, _number{ number }
|
||||||
|
, _count{ count }
|
||||||
|
, _track{ getDboPtr(track) }
|
||||||
|
{
|
||||||
|
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Movement name too long, truncated to '" << _name << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
Movement::pointer Movement::create(Session& session, std::string_view name, std::optional<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track)
|
||||||
|
{
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<Movement>{ new Movement{ name, number, count, track } });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::db
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "traits/IdTypeTraits.hpp"
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/ReleaseArtistLink.hpp"
|
#include "database/objects/ReleaseArtistLink.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|||||||
@@ -36,12 +36,14 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
@@ -673,6 +675,40 @@ namespace lms::db
|
|||||||
_moods.insert(getDboPtr(mood));
|
_moods.insert(getDboPtr(mood));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Track::setWorks(std::span<const ObjectPtr<Work>> works)
|
||||||
|
{
|
||||||
|
_works.clear();
|
||||||
|
for (const ObjectPtr<Work>& work : works)
|
||||||
|
_works.insert(getDboPtr(work));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Work::pointer> Track::getWorks() const
|
||||||
|
{
|
||||||
|
// deterministic order, callers rely on the first entry
|
||||||
|
return utils::fetchQueryResults<Work::pointer>(_works.find().orderBy("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Track::hasWork() const
|
||||||
|
{
|
||||||
|
return !_works.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::clearMovements()
|
||||||
|
{
|
||||||
|
_movements.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Movement::pointer> Track::getMovements() const
|
||||||
|
{
|
||||||
|
// deterministic order, callers rely on the first entry
|
||||||
|
return utils::fetchQueryResults<Movement::pointer>(_movements.find().orderBy("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Track::hasMovement() const
|
||||||
|
{
|
||||||
|
return !_movements.empty();
|
||||||
|
}
|
||||||
|
|
||||||
void Track::clearLyrics()
|
void Track::clearLyrics()
|
||||||
{
|
{
|
||||||
_trackLyrics.clear();
|
_trackLyrics.clear();
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
#include "database/objects/Grouping.hpp"
|
#include "database/objects/Grouping.hpp"
|
||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,11 @@
|
|||||||
#include "database/objects/Grouping.hpp"
|
#include "database/objects/Grouping.hpp"
|
||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/PlayListFile.hpp"
|
#include "database/objects/PlayListFile.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Impl.h>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/objects/Artist.hpp"
|
||||||
|
#include "database/objects/Artwork.hpp"
|
||||||
|
#include "database/objects/Cluster.hpp"
|
||||||
|
#include "database/objects/Directory.hpp"
|
||||||
|
#include "database/objects/Genre.hpp"
|
||||||
|
#include "database/objects/Grouping.hpp"
|
||||||
|
#include "database/objects/Language.hpp"
|
||||||
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
|
#include "database/objects/Medium.hpp"
|
||||||
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
|
#include "database/objects/Release.hpp"
|
||||||
|
#include "database/objects/Track.hpp"
|
||||||
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
#include "traits/UUIDTraits.hpp"
|
||||||
|
|
||||||
|
DBO_INSTANTIATE_TEMPLATES(lms::db::Work)
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
Work::Work(std::string_view name, const std::optional<core::UUID>& mbid)
|
||||||
|
: _mbid{ mbid }
|
||||||
|
{
|
||||||
|
setName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Work::setName(std::string_view name)
|
||||||
|
{
|
||||||
|
_name = name.substr(0, maxNameLength);
|
||||||
|
LMS_LOG_IF(DB, WARNING, name.size() > maxNameLength, "Work name too long, truncated to '" << _name << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
Work::pointer Work::create(Session& session, std::string_view name, const std::optional<core::UUID>& mbid)
|
||||||
|
{
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<Work>{ new Work{ name, mbid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
Work::pointer Work::find(Session& session, WorkId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<Work>().where("id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Work::pointer Work::find(Session& session, const core::UUID& mbid)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<Work>().where("mbid = ?").bind(mbid));
|
||||||
|
}
|
||||||
|
|
||||||
|
Work::pointer Work::find(Session& session, ReleaseId releaseId, std::string_view name)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
if (name.size() > maxNameLength)
|
||||||
|
name = name.substr(0, maxNameLength);
|
||||||
|
|
||||||
|
auto query{
|
||||||
|
session.getDboSession()->query<Wt::Dbo::ptr<Work>>("SELECT w FROM work w")
|
||||||
|
// clang-format off
|
||||||
|
.join("track_work t_w ON t_w.work_id = w.id")
|
||||||
|
.join("track t ON t.id = t_w.track_id")
|
||||||
|
.where("t.release_id = ?").bind(releaseId)
|
||||||
|
.where("w.name = ?").bind(std::string{ name })
|
||||||
|
.where("w.mbid IS NULL")
|
||||||
|
.groupBy("w.id")
|
||||||
|
// clang-format on
|
||||||
|
};
|
||||||
|
|
||||||
|
return utils::fetchQuerySingleResult(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<WorkId> Work::findOrphanIds(Session& session, std::optional<Range> range)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
auto query{ session.getDboSession()->query<WorkId>("SELECT w.id FROM work w WHERE NOT EXISTS (SELECT 1 FROM track_work t_w WHERE t_w.work_id = w.id)") };
|
||||||
|
return utils::execRangeQuery<WorkId>(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::db
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Field.h>
|
||||||
|
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
#include "database/objects/MovementId.hpp"
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
class Track;
|
||||||
|
|
||||||
|
class Movement final : public Object<Movement, MovementId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr std::size_t maxNameLength{ 512 };
|
||||||
|
|
||||||
|
Movement() = default;
|
||||||
|
|
||||||
|
static pointer create(Session& session, std::string_view name, std::optional<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track);
|
||||||
|
|
||||||
|
std::string_view getName() const { return _name; }
|
||||||
|
std::optional<std::size_t> getNumber() const { return _number; }
|
||||||
|
std::optional<std::size_t> getCount() const { return _count; }
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
Wt::Dbo::field(a, _number, "number");
|
||||||
|
Wt::Dbo::field(a, _count, "count");
|
||||||
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Session;
|
||||||
|
Movement(std::string_view name, std::optional<std::size_t> number, std::optional<std::size_t> count, const ObjectPtr<Track>& track);
|
||||||
|
|
||||||
|
std::string _name;
|
||||||
|
std::optional<int> _number;
|
||||||
|
std::optional<int> _count;
|
||||||
|
Wt::Dbo::ptr<Track> _track;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lms::db
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(MovementId)
|
||||||
@@ -51,12 +51,14 @@
|
|||||||
#include "database/objects/MediaLibraryId.hpp"
|
#include "database/objects/MediaLibraryId.hpp"
|
||||||
#include "database/objects/MediumId.hpp"
|
#include "database/objects/MediumId.hpp"
|
||||||
#include "database/objects/MoodId.hpp"
|
#include "database/objects/MoodId.hpp"
|
||||||
|
#include "database/objects/MovementId.hpp"
|
||||||
#include "database/objects/ReleaseId.hpp"
|
#include "database/objects/ReleaseId.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImageId.hpp"
|
#include "database/objects/TrackEmbeddedImageId.hpp"
|
||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
#include "database/objects/TrackListId.hpp"
|
#include "database/objects/TrackListId.hpp"
|
||||||
#include "database/objects/Types.hpp"
|
#include "database/objects/Types.hpp"
|
||||||
#include "database/objects/UserId.hpp"
|
#include "database/objects/UserId.hpp"
|
||||||
|
#include "database/objects/WorkId.hpp"
|
||||||
#include "database/objects/detail/Types.hpp"
|
#include "database/objects/detail/Types.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -69,16 +71,18 @@ namespace lms::db
|
|||||||
class Genre;
|
class Genre;
|
||||||
class Grouping;
|
class Grouping;
|
||||||
class Language;
|
class Language;
|
||||||
|
class Medium;
|
||||||
|
class Movement;
|
||||||
class Mood;
|
class Mood;
|
||||||
class TrackEmbeddedImageLink;
|
class TrackEmbeddedImageLink;
|
||||||
class MediaLibrary;
|
class MediaLibrary;
|
||||||
class Medium;
|
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class TrackArtistLink;
|
class TrackArtistLink;
|
||||||
class TrackLyrics;
|
class TrackLyrics;
|
||||||
class TrackStats;
|
class TrackStats;
|
||||||
class User;
|
class User;
|
||||||
|
class Work;
|
||||||
|
|
||||||
class Track final : public Object<Track, TrackId>
|
class Track final : public Object<Track, TrackId>
|
||||||
{
|
{
|
||||||
@@ -279,6 +283,8 @@ namespace lms::db
|
|||||||
void setGroupings(std::span<const ObjectPtr<Grouping>> groupings);
|
void setGroupings(std::span<const ObjectPtr<Grouping>> groupings);
|
||||||
void setLanguages(std::span<const ObjectPtr<Language>> languages);
|
void setLanguages(std::span<const ObjectPtr<Language>> languages);
|
||||||
void setMoods(std::span<const ObjectPtr<Mood>> moods);
|
void setMoods(std::span<const ObjectPtr<Mood>> moods);
|
||||||
|
void setWorks(std::span<const ObjectPtr<Work>> works);
|
||||||
|
void clearMovements();
|
||||||
void clearLyrics();
|
void clearLyrics();
|
||||||
void clearEmbeddedLyrics();
|
void clearEmbeddedLyrics();
|
||||||
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
||||||
@@ -310,7 +316,7 @@ namespace lms::db
|
|||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||||
std::string getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
const core::PartialDateTime& getDate() const { return _date; }
|
const core::PartialDateTime& getDate() const { return _date; }
|
||||||
std::optional<int> getYear() const;
|
std::optional<int> getYear() const;
|
||||||
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
|
const core::PartialDateTime& getOriginalDate() const { return _originalDate; }
|
||||||
@@ -347,6 +353,10 @@ namespace lms::db
|
|||||||
std::vector<LanguageId> getLanguageIds() const;
|
std::vector<LanguageId> getLanguageIds() const;
|
||||||
std::vector<ObjectPtr<Mood>> getMoods() const;
|
std::vector<ObjectPtr<Mood>> getMoods() const;
|
||||||
std::vector<MoodId> getMoodIds() const;
|
std::vector<MoodId> getMoodIds() const;
|
||||||
|
std::vector<ObjectPtr<Work>> getWorks() const;
|
||||||
|
bool hasWork() const;
|
||||||
|
std::vector<ObjectPtr<Movement>> getMovements() const;
|
||||||
|
bool hasMovement() const;
|
||||||
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
||||||
ObjectPtr<Directory> getDirectory() const;
|
ObjectPtr<Directory> getDirectory() const;
|
||||||
ObjectPtr<Artwork> getPreferredArtwork() const;
|
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||||
@@ -393,12 +403,14 @@ namespace lms::db
|
|||||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull);
|
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
|
Wt::Dbo::hasMany(a, _movements, Wt::Dbo::ManyToOne, "track");
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _genres, Wt::Dbo::ManyToMany, "track_genre", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _groupings, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _groupings, Wt::Dbo::ManyToMany, "track_grouping", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _languages, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _languages, Wt::Dbo::ManyToMany, "track_language", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _moods, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _moods, Wt::Dbo::ManyToMany, "track_mood", "", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::hasMany(a, _works, Wt::Dbo::ManyToMany, "track_work", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
||||||
Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track");
|
||||||
}
|
}
|
||||||
@@ -441,7 +453,6 @@ namespace lms::db
|
|||||||
std::string _artistDisplayName;
|
std::string _artistDisplayName;
|
||||||
std::string _comment;
|
std::string _comment;
|
||||||
Advisory _advisory{ Advisory::UnSet };
|
Advisory _advisory{ Advisory::UnSet };
|
||||||
|
|
||||||
Wt::Dbo::ptr<Medium> _medium;
|
Wt::Dbo::ptr<Medium> _medium;
|
||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||||
@@ -454,6 +465,8 @@ namespace lms::db
|
|||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Grouping>> _groupings;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Language>> _languages;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Mood>> _moods;
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Work>> _works;
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Movement>> _movements;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <Wt/Dbo/Field.h>
|
||||||
|
#include <Wt/Dbo/collection.h>
|
||||||
|
|
||||||
|
#include "core/UUID.hpp"
|
||||||
|
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
#include "database/Types.hpp"
|
||||||
|
#include "database/objects/ReleaseId.hpp"
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
#include "database/objects/WorkId.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Session;
|
||||||
|
class Track;
|
||||||
|
|
||||||
|
class Work final : public Object<Work, WorkId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr std::size_t maxNameLength{ 512 };
|
||||||
|
|
||||||
|
Work() = default;
|
||||||
|
|
||||||
|
static pointer find(Session& session, WorkId id);
|
||||||
|
// Global lookup: MusicBrainz Work Id is a strong, unambiguous identity shared across the whole library
|
||||||
|
static pointer find(Session& session, const core::UUID& mbid);
|
||||||
|
// Name-only lookup, scoped to works already linked to a track of the given release: work titles are
|
||||||
|
// often generic (e.g. "Symphony No. 5") and collide across unrelated works, so without an mbid we only
|
||||||
|
// ever match within the same release instead of matching globally by name
|
||||||
|
static pointer find(Session& session, ReleaseId releaseId, std::string_view name);
|
||||||
|
static std::vector<WorkId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
|
void setName(std::string_view name);
|
||||||
|
|
||||||
|
std::string_view getName() const { return _name; }
|
||||||
|
std::optional<core::UUID> getMBID() const { return _mbid; }
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
Wt::Dbo::field(a, _mbid, "mbid");
|
||||||
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_work", "", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Session;
|
||||||
|
Work(std::string_view name, const std::optional<core::UUID>& mbid);
|
||||||
|
static pointer create(Session& session, std::string_view name, const std::optional<core::UUID>& mbid);
|
||||||
|
|
||||||
|
std::string _name;
|
||||||
|
std::optional<core::UUID> _mbid;
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lms::db
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(WorkId)
|
||||||
@@ -16,6 +16,7 @@ add_executable(test-database
|
|||||||
Medium.cpp
|
Medium.cpp
|
||||||
Migration.cpp
|
Migration.cpp
|
||||||
Mood.cpp
|
Mood.cpp
|
||||||
|
Movement.cpp
|
||||||
PlayListFile.cpp
|
PlayListFile.cpp
|
||||||
Podcast.cpp
|
Podcast.cpp
|
||||||
RatedArtist.cpp
|
RatedArtist.cpp
|
||||||
@@ -29,6 +30,7 @@ add_executable(test-database
|
|||||||
StarredTrack.cpp
|
StarredTrack.cpp
|
||||||
Track.cpp
|
Track.cpp
|
||||||
TrackArtistLink.cpp
|
TrackArtistLink.cpp
|
||||||
|
Work.cpp
|
||||||
TrackBookmark.cpp
|
TrackBookmark.cpp
|
||||||
TrackEmbeddedImage.cpp
|
TrackEmbeddedImage.cpp
|
||||||
TrackList.cpp
|
TrackList.cpp
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
|
#include "database/objects/Track.hpp"
|
||||||
|
|
||||||
|
namespace lms::db::tests
|
||||||
|
{
|
||||||
|
TEST_F(DatabaseFixture, Movement_create)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "Allegro con brio", std::size_t{ 1 }, std::size_t{ 4 }, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
ASSERT_EQ(movements.size(), 1);
|
||||||
|
ASSERT_FALSE(movements[0]->getName().empty());
|
||||||
|
EXPECT_EQ(movements[0]->getName(), "Allegro con brio");
|
||||||
|
ASSERT_TRUE(movements[0]->getNumber());
|
||||||
|
EXPECT_EQ(*movements[0]->getNumber(), std::size_t{ 1 });
|
||||||
|
ASSERT_TRUE(movements[0]->getCount());
|
||||||
|
EXPECT_EQ(*movements[0]->getCount(), std::size_t{ 4 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_createWithNullFields)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "", std::nullopt, std::nullopt, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
ASSERT_EQ(movements.size(), 1);
|
||||||
|
EXPECT_TRUE(movements[0]->getName().empty());
|
||||||
|
EXPECT_FALSE(movements[0]->getNumber());
|
||||||
|
EXPECT_FALSE(movements[0]->getCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_multipleMovementsOnTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "Allegro con brio", std::size_t{ 1 }, std::size_t{ 4 }, track.get());
|
||||||
|
Movement::create(session, "Andante con moto", std::size_t{ 2 }, std::size_t{ 4 }, track.get());
|
||||||
|
Movement::create(session, "Scherzo. Allegro", std::size_t{ 3 }, std::size_t{ 4 }, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
EXPECT_EQ(movements.size(), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_cascadeDeleteWithTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "Allegro", std::size_t{ 1 }, std::nullopt, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getMovements().size(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// track goes out of scope here — movements are cascade deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_clearMovements)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "Allegro", std::size_t{ 1 }, std::size_t{ 2 }, track.get());
|
||||||
|
Movement::create(session, "Andante", std::size_t{ 2 }, std::size_t{ 2 }, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getMovements().size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->clearMovements();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getMovements().size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_replacingMovements)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "OldMovement", std::size_t{ 1 }, std::nullopt, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->clearMovements();
|
||||||
|
Movement::create(session, "NewMovement", std::size_t{ 1 }, std::nullopt, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
ASSERT_EQ(movements.size(), 1);
|
||||||
|
ASSERT_FALSE(movements[0]->getName().empty());
|
||||||
|
EXPECT_EQ(movements[0]->getName(), "NewMovement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Movement_getNumberReturnsCorrectSizeT)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Movement::create(session, "", std::size_t{ 42 }, std::size_t{ 100 }, track.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
ASSERT_EQ(movements.size(), 1);
|
||||||
|
ASSERT_TRUE(movements[0]->getNumber());
|
||||||
|
EXPECT_EQ(*movements[0]->getNumber(), std::size_t{ 42 });
|
||||||
|
ASSERT_TRUE(movements[0]->getCount());
|
||||||
|
EXPECT_EQ(*movements[0]->getCount(), std::size_t{ 100 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::db::tests
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
|
namespace lms::db::tests
|
||||||
|
{
|
||||||
|
using ScopedWork = ScopedEntity<db::Work>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_create)
|
||||||
|
{
|
||||||
|
ScopedWork work{ session, "Symphony No. 5", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const Work::pointer found{ Work::find(session, work.getId()) };
|
||||||
|
ASSERT_TRUE(found);
|
||||||
|
EXPECT_EQ(found->getName(), "Symphony No. 5");
|
||||||
|
EXPECT_FALSE(found->getMBID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_createWithMBID)
|
||||||
|
{
|
||||||
|
const auto mbid{ core::UUID::fromString("8f3471b3-7e93-4de1-a9c7-3b843c21b84e") };
|
||||||
|
ASSERT_TRUE(mbid);
|
||||||
|
|
||||||
|
ScopedWork work{ session, "Symphony No. 9", mbid };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const Work::pointer found{ Work::find(session, work.getId()) };
|
||||||
|
ASSERT_TRUE(found);
|
||||||
|
EXPECT_EQ(found->getName(), "Symphony No. 9");
|
||||||
|
ASSERT_TRUE(found->getMBID());
|
||||||
|
EXPECT_EQ(found->getMBID(), mbid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_findByMBID)
|
||||||
|
{
|
||||||
|
const auto mbid{ core::UUID::fromString("8f3471b3-7e93-4de1-a9c7-3b843c21b84e") };
|
||||||
|
ASSERT_TRUE(mbid);
|
||||||
|
|
||||||
|
ScopedWork workWithMbid{ session, "Piano Sonata", mbid };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const Work::pointer byMbid{ Work::find(session, *mbid) };
|
||||||
|
ASSERT_TRUE(byMbid);
|
||||||
|
EXPECT_EQ(byMbid->getId(), workWithMbid.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_findByNameScopedToRelease)
|
||||||
|
{
|
||||||
|
// Work titles are often generic (e.g. "Piano Sonata"): two unrelated releases can each have
|
||||||
|
// their own work with the exact same name, and lookup must not merge them
|
||||||
|
ScopedRelease release1{ session, "Release1" };
|
||||||
|
ScopedRelease release2{ session, "Release2" };
|
||||||
|
ScopedTrack track1{ session };
|
||||||
|
ScopedTrack track2{ session };
|
||||||
|
ScopedWork work1{ session, "Piano Sonata", std::optional<core::UUID>{} };
|
||||||
|
ScopedWork work2{ session, "Piano Sonata", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track1.get().modify()->setRelease(release1.get());
|
||||||
|
track1.get().modify()->setWorks(std::array{ work1.get() });
|
||||||
|
track2.get().modify()->setRelease(release2.get());
|
||||||
|
track2.get().modify()->setWorks(std::array{ work2.get() });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
// Same name, but each release resolves to its own work
|
||||||
|
const Work::pointer foundInRelease1{ Work::find(session, release1.getId(), "Piano Sonata") };
|
||||||
|
ASSERT_TRUE(foundInRelease1);
|
||||||
|
EXPECT_EQ(foundInRelease1->getId(), work1.getId());
|
||||||
|
|
||||||
|
const Work::pointer foundInRelease2{ Work::find(session, release2.getId(), "Piano Sonata") };
|
||||||
|
ASSERT_TRUE(foundInRelease2);
|
||||||
|
EXPECT_EQ(foundInRelease2->getId(), work2.getId());
|
||||||
|
|
||||||
|
// Unknown name in a known release returns null
|
||||||
|
EXPECT_FALSE(Work::find(session, release1.getId(), "Unknown Work"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_orphan)
|
||||||
|
{
|
||||||
|
ScopedWork work{ session, "Unlinked Work", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
const auto orphans{ Work::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(orphans.size(), 1);
|
||||||
|
EXPECT_EQ(orphans.front(), work.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_singleTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
ScopedWork work1{ session, "Requiem", std::optional<core::UUID>{} };
|
||||||
|
ScopedWork work2{ session, "Missa Solemnis", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(Work::findOrphanIds(session).size(), 2);
|
||||||
|
EXPECT_EQ(track->getWorks().size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setWorks(std::array{ work1.get() });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto orphans{ Work::findOrphanIds(session) };
|
||||||
|
ASSERT_EQ(orphans.size(), 1);
|
||||||
|
EXPECT_EQ(orphans.front(), work2.getId());
|
||||||
|
|
||||||
|
const auto trackWorks{ track->getWorks() };
|
||||||
|
ASSERT_EQ(trackWorks.size(), 1);
|
||||||
|
EXPECT_EQ(trackWorks.front()->getId(), work1.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_multipleWorksOnTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
ScopedWork work1{ session, "Symphony No. 5", std::optional<core::UUID>{} };
|
||||||
|
ScopedWork work2{ session, "Symphony No. 6", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setWorks(std::array{ work1.get(), work2.get() });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(Work::findOrphanIds(session).size(), 0);
|
||||||
|
|
||||||
|
const auto trackWorks{ track->getWorks() };
|
||||||
|
EXPECT_EQ(trackWorks.size(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_multipleTracksOnWork)
|
||||||
|
{
|
||||||
|
ScopedTrack track1{ session };
|
||||||
|
ScopedTrack track2{ session };
|
||||||
|
ScopedTrack track3{ session };
|
||||||
|
ScopedWork work{ session, "The Four Seasons", std::optional<core::UUID>{} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track1.get().modify()->setWorks(std::array{ work.get() });
|
||||||
|
track2.get().modify()->setWorks(std::array{ work.get() });
|
||||||
|
track3.get().modify()->setWorks(std::array{ work.get() });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(Work::findOrphanIds(session).size(), 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(track1->getWorks().size(), 1);
|
||||||
|
EXPECT_EQ(track2->getWorks().size(), 1);
|
||||||
|
EXPECT_EQ(track3->getWorks().size(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Work_nameTruncation)
|
||||||
|
{
|
||||||
|
const std::string longName(Work::maxNameLength + 100, 'x');
|
||||||
|
const std::string expectedName(Work::maxNameLength, 'x');
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Work::pointer work{ session.create<Work>(longName, std::optional<core::UUID>{}) };
|
||||||
|
ASSERT_TRUE(work);
|
||||||
|
EXPECT_EQ(work->getName().size(), Work::maxNameLength);
|
||||||
|
EXPECT_EQ(work->getName(), expectedName);
|
||||||
|
work.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::db::tests
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/ReleaseArtistLink.hpp"
|
#include "database/objects/ReleaseArtistLink.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/objects/TrackLyrics.hpp"
|
#include "database/objects/TrackLyrics.hpp"
|
||||||
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
|
|
||||||
@@ -368,6 +370,24 @@ namespace lms::scanner
|
|||||||
return moods;
|
return moods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<db::Work::pointer> getOrCreateWorks(db::Session& session, db::ReleaseId releaseId, std::span<const Work> works)
|
||||||
|
{
|
||||||
|
std::vector<db::Work::pointer> dbWorks;
|
||||||
|
dbWorks.reserve(works.size());
|
||||||
|
for (const Work& work : works)
|
||||||
|
{
|
||||||
|
// Work titles are often generic and collide across unrelated works, so
|
||||||
|
// without an mbid we only ever match a work already used on the same release, not globally by name
|
||||||
|
db::Work::pointer dbWork{ work.mbid ? db::Work::find(session, *work.mbid) : (releaseId.isValid() ? db::Work::find(session, releaseId, work.name) : db::Work::pointer{}) };
|
||||||
|
if (!dbWork)
|
||||||
|
dbWork = session.create<db::Work>(work.name, work.mbid);
|
||||||
|
else if (dbWork->getName() != work.name)
|
||||||
|
dbWork.modify()->setName(work.name);
|
||||||
|
dbWorks.push_back(dbWork);
|
||||||
|
}
|
||||||
|
return dbWorks;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
|
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const Track& track)
|
||||||
{
|
{
|
||||||
std::vector<db::Cluster::pointer> clusters;
|
std::vector<db::Cluster::pointer> clusters;
|
||||||
@@ -836,6 +856,12 @@ namespace lms::scanner
|
|||||||
track.modify()->setGroupings(getOrCreateGroupings(dbSession, _file->track.groupings));
|
track.modify()->setGroupings(getOrCreateGroupings(dbSession, _file->track.groupings));
|
||||||
track.modify()->setLanguages(getOrCreateLanguages(dbSession, _file->track.languages));
|
track.modify()->setLanguages(getOrCreateLanguages(dbSession, _file->track.languages));
|
||||||
track.modify()->setMoods(getOrCreateMoods(dbSession, _file->track.moods));
|
track.modify()->setMoods(getOrCreateMoods(dbSession, _file->track.moods));
|
||||||
|
track.modify()->setWorks(getOrCreateWorks(dbSession, track->getReleaseId(), _file->track.works));
|
||||||
|
|
||||||
|
track.modify()->clearMovements();
|
||||||
|
for (const auto& movement : _file->track.movements)
|
||||||
|
db::Movement::create(dbSession, movement.name, movement.number, movement.count, track);
|
||||||
|
|
||||||
track.modify()->setName(title);
|
track.modify()->setName(title);
|
||||||
track.modify()->setTrackNumber(_file->track.position);
|
track.modify()->setTrackNumber(_file->track.position);
|
||||||
track.modify()->setDate(_file->track.date);
|
track.modify()->setDate(_file->track.date);
|
||||||
|
|||||||
@@ -184,6 +184,38 @@ namespace lms::scanner
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Work> getWorks(const audio::ITagReader& tagReader)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> titles{ getTagValuesAs<std::string>(tagReader, audio::TagType::WorkTitle, {}) };
|
||||||
|
const std::vector<core::UUID> mbids{ getTagValuesAs<core::UUID>(tagReader, audio::TagType::MusicBrainzWorkID, {}) };
|
||||||
|
|
||||||
|
const bool mbidsMatch{ mbids.size() == titles.size() };
|
||||||
|
|
||||||
|
std::vector<Work> works;
|
||||||
|
works.reserve(titles.size());
|
||||||
|
for (std::size_t i{}; i < titles.size(); ++i)
|
||||||
|
works.push_back({ mbidsMatch ? std::optional{ mbids[i] } : std::nullopt, titles[i] });
|
||||||
|
|
||||||
|
return works;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Track::MovementData> getMovements(const audio::ITagReader& tagReader)
|
||||||
|
{
|
||||||
|
const std::vector<std::string> names{ getTagValuesAs<std::string>(tagReader, audio::TagType::Movement, {}) };
|
||||||
|
const std::vector<std::size_t> numbers{ getTagValuesAs<std::size_t>(tagReader, audio::TagType::MovementNumber, {}) };
|
||||||
|
const std::vector<std::size_t> counts{ getTagValuesAs<std::size_t>(tagReader, audio::TagType::MovementCount, {}) };
|
||||||
|
|
||||||
|
const bool numbersMatch{ numbers.size() == names.size() };
|
||||||
|
const bool countsMatch{ counts.size() == names.size() };
|
||||||
|
|
||||||
|
std::vector<Track::MovementData> movements;
|
||||||
|
movements.reserve(names.size());
|
||||||
|
for (std::size_t i{}; i < names.size(); ++i)
|
||||||
|
movements.push_back({ names[i], numbersMatch ? std::optional{ numbers[i] } : std::nullopt, countsMatch ? std::optional{ counts[i] } : std::nullopt });
|
||||||
|
|
||||||
|
return movements;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
|
std::vector<Artist> getArtists(const audio::ITagReader& tagReader,
|
||||||
std::initializer_list<audio::TagType> artistTagNames,
|
std::initializer_list<audio::TagType> artistTagNames,
|
||||||
std::initializer_list<audio::TagType> artistSortTagNames,
|
std::initializer_list<audio::TagType> artistSortTagNames,
|
||||||
@@ -328,6 +360,8 @@ namespace lms::scanner
|
|||||||
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
|
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
|
||||||
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
|
||||||
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
|
||||||
|
track.works = getWorks(tagReader);
|
||||||
|
track.movements = getMovements(tagReader);
|
||||||
track.acoustID = getTagValueAs<core::UUID>(tagReader, TagType::AcoustID);
|
track.acoustID = getTagValueAs<core::UUID>(tagReader, TagType::AcoustID);
|
||||||
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
|
track.position = getTagValueAs<std::size_t>(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
|
||||||
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::Date) })
|
if (const auto dateStr{ getTagValueAs<std::string>(tagReader, TagType::Date) })
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
|
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ namespace lms::scanner
|
|||||||
removeOrphanedGroupings(context);
|
removeOrphanedGroupings(context);
|
||||||
removeOrphanedLanguages(context);
|
removeOrphanedLanguages(context);
|
||||||
removeOrphanedMoods(context);
|
removeOrphanedMoods(context);
|
||||||
|
removeOrphanedWorks(context);
|
||||||
removeOrphanedArtists(context);
|
removeOrphanedArtists(context);
|
||||||
removeOrphanedReleases(context);
|
removeOrphanedReleases(context);
|
||||||
removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key
|
removeOrphanedMediums(context); // after release so that most entries are removed using the medium foreign key
|
||||||
@@ -98,6 +100,12 @@ namespace lms::scanner
|
|||||||
removeOrphanedEntries<db::Mood>(context);
|
removeOrphanedEntries<db::Mood>(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedWorks(ScanContext& context)
|
||||||
|
{
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned works...");
|
||||||
|
removeOrphanedEntries<db::Work>(context);
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists(ScanContext& context)
|
void ScanStepRemoveOrphanedDbEntries::removeOrphanedArtists(ScanContext& context)
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists...");
|
LMS_LOG(DBUPDATER, DEBUG, "Checking orphaned artists...");
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ namespace lms::scanner
|
|||||||
void removeOrphanedGroupings(ScanContext& context);
|
void removeOrphanedGroupings(ScanContext& context);
|
||||||
void removeOrphanedLanguages(ScanContext& context);
|
void removeOrphanedLanguages(ScanContext& context);
|
||||||
void removeOrphanedMoods(ScanContext& context);
|
void removeOrphanedMoods(ScanContext& context);
|
||||||
|
void removeOrphanedWorks(ScanContext& context);
|
||||||
void removeOrphanedArtists(ScanContext& context);
|
void removeOrphanedArtists(ScanContext& context);
|
||||||
void removeOrphanedMediums(ScanContext& context);
|
void removeOrphanedMediums(ScanContext& context);
|
||||||
void removeOrphanedReleases(ScanContext& context);
|
void removeOrphanedReleases(ScanContext& context);
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ namespace lms::scanner
|
|||||||
|
|
||||||
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
|
||||||
|
|
||||||
|
struct Work
|
||||||
|
{
|
||||||
|
std::optional<core::UUID> mbid;
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
auto operator<=>(const Work&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct Release
|
struct Release
|
||||||
{
|
{
|
||||||
std::optional<core::UUID> mbid;
|
std::optional<core::UUID> mbid;
|
||||||
@@ -100,8 +108,17 @@ namespace lms::scanner
|
|||||||
Clean,
|
Clean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct MovementData
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::optional<std::size_t> number;
|
||||||
|
std::optional<std::size_t> count;
|
||||||
|
};
|
||||||
|
|
||||||
std::optional<core::UUID> mbid;
|
std::optional<core::UUID> mbid;
|
||||||
std::optional<core::UUID> recordingMBID;
|
std::optional<core::UUID> recordingMBID;
|
||||||
|
std::vector<Work> works;
|
||||||
|
std::vector<MovementData> movements;
|
||||||
std::string title;
|
std::string title;
|
||||||
std::optional<Medium> medium;
|
std::optional<Medium> medium;
|
||||||
std::optional<std::size_t> position; // in medium
|
std::optional<std::size_t> position; // in medium
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ namespace lms::scanner::tests
|
|||||||
{ TagType::MusicBrainzReleaseID, { "3fa39992-b786-4585-a70e-85d5cc15ef69" } },
|
{ TagType::MusicBrainzReleaseID, { "3fa39992-b786-4585-a70e-85d5cc15ef69" } },
|
||||||
{ TagType::MusicBrainzReleaseGroupID, { "5b1a5a44-8420-4426-9b86-d25dc8d04838" } },
|
{ TagType::MusicBrainzReleaseGroupID, { "5b1a5a44-8420-4426-9b86-d25dc8d04838" } },
|
||||||
{ TagType::MusicBrainzRecordingID, { "bd3fc666-89de-4ac8-93f6-2dbf028ad8d5" } },
|
{ TagType::MusicBrainzRecordingID, { "bd3fc666-89de-4ac8-93f6-2dbf028ad8d5" } },
|
||||||
|
{ TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888", "aaaabbbb-cccc-dddd-eeee-ffff00001111" } },
|
||||||
|
{ TagType::WorkTitle, { "MyWork1", "MyWork2" } },
|
||||||
|
{ TagType::Movement, { "Allegro con brio", "Andante" } },
|
||||||
|
{ TagType::MovementNumber, { "1", "2" } },
|
||||||
|
{ TagType::MovementCount, { "4", "4" } },
|
||||||
{ TagType::Producer, { "MyProducer1", "MyProducer2" } },
|
{ TagType::Producer, { "MyProducer1", "MyProducer2" } },
|
||||||
{ TagType::Remixer, { "MyRemixer1", "MyRemixer2" } },
|
{ TagType::Remixer, { "MyRemixer1", "MyRemixer2" } },
|
||||||
{ TagType::RecordLabel, { "Label1", "Label2" } },
|
{ TagType::RecordLabel, { "Label1", "Label2" } },
|
||||||
|
|||||||
@@ -114,6 +114,24 @@ namespace lms::scanner::tests
|
|||||||
EXPECT_EQ(track.producerArtists[1].name, "MyProducer2");
|
EXPECT_EQ(track.producerArtists[1].name, "MyProducer2");
|
||||||
ASSERT_TRUE(track.recordingMBID.has_value());
|
ASSERT_TRUE(track.recordingMBID.has_value());
|
||||||
EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
|
EXPECT_EQ(track.recordingMBID.value(), core::UUID::fromString("bd3fc666-89de-4ac8-93f6-2dbf028ad8d5"));
|
||||||
|
ASSERT_EQ(track.works.size(), 2);
|
||||||
|
EXPECT_EQ(track.works[0].name, "MyWork1");
|
||||||
|
EXPECT_EQ(track.works[1].name, "MyWork2");
|
||||||
|
ASSERT_TRUE(track.works[0].mbid.has_value());
|
||||||
|
EXPECT_EQ(track.works[0].mbid.value(), core::UUID::fromString("11112222-3333-4444-5555-666677778888"));
|
||||||
|
ASSERT_TRUE(track.works[1].mbid.has_value());
|
||||||
|
EXPECT_EQ(track.works[1].mbid.value(), core::UUID::fromString("aaaabbbb-cccc-dddd-eeee-ffff00001111"));
|
||||||
|
ASSERT_EQ(track.movements.size(), 2);
|
||||||
|
EXPECT_EQ(track.movements[0].name, "Allegro con brio");
|
||||||
|
ASSERT_TRUE(track.movements[0].number.has_value());
|
||||||
|
EXPECT_EQ(track.movements[0].number.value(), 1);
|
||||||
|
ASSERT_TRUE(track.movements[0].count.has_value());
|
||||||
|
EXPECT_EQ(track.movements[0].count.value(), 4);
|
||||||
|
EXPECT_EQ(track.movements[1].name, "Andante");
|
||||||
|
ASSERT_TRUE(track.movements[1].number.has_value());
|
||||||
|
EXPECT_EQ(track.movements[1].number.value(), 2);
|
||||||
|
ASSERT_TRUE(track.movements[1].count.has_value());
|
||||||
|
EXPECT_EQ(track.movements[1].count.value(), 4);
|
||||||
ASSERT_TRUE(track.replayGain.has_value());
|
ASSERT_TRUE(track.replayGain.has_value());
|
||||||
EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33);
|
EXPECT_FLOAT_EQ(track.replayGain.value(), -0.33);
|
||||||
ASSERT_EQ(track.remixerArtists.size(), 2);
|
ASSERT_EQ(track.remixerArtists.size(), 2);
|
||||||
@@ -720,6 +738,60 @@ namespace lms::scanner::tests
|
|||||||
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
EXPECT_EQ(track.artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(TrackMetadataParser, works_allMbidsPresent)
|
||||||
|
{
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } },
|
||||||
|
{ audio::TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888", "aaaabbbb-cccc-dddd-eeee-ffff00001111" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track.works.size(), 2);
|
||||||
|
EXPECT_EQ(track.works[0].name, "MyWork1");
|
||||||
|
EXPECT_EQ(track.works[0].mbid, core::UUID::fromString("11112222-3333-4444-5555-666677778888"));
|
||||||
|
EXPECT_EQ(track.works[1].name, "MyWork2");
|
||||||
|
EXPECT_EQ(track.works[1].mbid, core::UUID::fromString("aaaabbbb-cccc-dddd-eeee-ffff00001111"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrackMetadataParser, works_noMbids)
|
||||||
|
{
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track.works.size(), 2);
|
||||||
|
EXPECT_EQ(track.works[0].name, "MyWork1");
|
||||||
|
EXPECT_EQ(track.works[0].mbid, std::nullopt);
|
||||||
|
EXPECT_EQ(track.works[1].name, "MyWork2");
|
||||||
|
EXPECT_EQ(track.works[1].mbid, std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TrackMetadataParser, works_mismatchedMbidCount)
|
||||||
|
{
|
||||||
|
// mbid count does not match work title count => mbids are all discarded (all or nothing)
|
||||||
|
const TestTagReader testTags{
|
||||||
|
{
|
||||||
|
{ audio::TagType::WorkTitle, { "MyWork1", "MyWork2" } },
|
||||||
|
{ audio::TagType::MusicBrainzWorkID, { "11112222-3333-4444-5555-666677778888" } },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Track track{ TrackMetadataParser{}.parseTrackMetaData(testTags) };
|
||||||
|
|
||||||
|
ASSERT_EQ(track.works.size(), 2);
|
||||||
|
EXPECT_EQ(track.works[0].name, "MyWork1");
|
||||||
|
EXPECT_EQ(track.works[0].mbid, std::nullopt);
|
||||||
|
EXPECT_EQ(track.works[1].name, "MyWork2");
|
||||||
|
EXPECT_EQ(track.works[1].mbid, std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(TrackMetadataParser, heterogeneousArtistMbids)
|
TEST(TrackMetadataParser, heterogeneousArtistMbids)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ namespace lms::scrobbling::listenBrainz
|
|||||||
Wt::Json::Object trackMetadata;
|
Wt::Json::Object trackMetadata;
|
||||||
trackMetadata["additional_info"] = std::move(additionalInfo);
|
trackMetadata["additional_info"] = std::move(additionalInfo);
|
||||||
trackMetadata["artist_name"] = Wt::Json::Value{ std::string{ track->getArtistDisplayName() } };
|
trackMetadata["artist_name"] = Wt::Json::Value{ std::string{ track->getArtistDisplayName() } };
|
||||||
trackMetadata["track_name"] = Wt::Json::Value{ track->getName() };
|
trackMetadata["track_name"] = Wt::Json::Value{ std::string{ track->getName() } };
|
||||||
if (track->getRelease())
|
if (track->getRelease())
|
||||||
trackMetadata["release_name"] = Wt::Json::Value{ std::string{ track->getRelease()->getName() } };
|
trackMetadata["release_name"] = Wt::Json::Value{ std::string{ track->getRelease()->getName() } };
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,13 @@
|
|||||||
#include "database/objects/MediaLibrary.hpp"
|
#include "database/objects/MediaLibrary.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/ReleaseArtistLink.hpp"
|
#include "database/objects/ReleaseArtistLink.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
|
||||||
@@ -250,6 +252,28 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
trackResponse.addChild("replayGain", createReplayGainNode(track, medium));
|
trackResponse.addChild("replayGain", createReplayGainNode(track, medium));
|
||||||
|
|
||||||
|
trackResponse.createEmptyArrayChild("works");
|
||||||
|
for (const auto& work : track->getWorks())
|
||||||
|
{
|
||||||
|
Response::Node workNode;
|
||||||
|
workNode.setAttribute("name", work->getName());
|
||||||
|
if (const auto mbid{ work->getMBID() })
|
||||||
|
workNode.setAttribute("musicBrainzId", mbid->toString());
|
||||||
|
trackResponse.addArrayChild("works", std::move(workNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
trackResponse.createEmptyArrayChild("movements");
|
||||||
|
for (const auto& movement : track->getMovements())
|
||||||
|
{
|
||||||
|
Response::Node movementNode;
|
||||||
|
movementNode.setAttribute("name", movement->getName());
|
||||||
|
if (const auto n{ movement->getNumber() })
|
||||||
|
movementNode.setAttribute("number", *n);
|
||||||
|
if (const auto c{ movement->getCount() })
|
||||||
|
movementNode.setAttribute("count", *c);
|
||||||
|
trackResponse.addArrayChild("movements", std::move(movementNode));
|
||||||
|
}
|
||||||
|
|
||||||
return trackResponse;
|
return trackResponse;
|
||||||
}
|
}
|
||||||
} // namespace lms::api::subsonic
|
} // namespace lms::api::subsonic
|
||||||
@@ -244,6 +244,7 @@ namespace lms::ui
|
|||||||
const std::string nativeResource{ _audioFileResource->getUrl(trackId) };
|
const std::string nativeResource{ _audioFileResource->getUrl(trackId) };
|
||||||
|
|
||||||
const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
|
const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
|
||||||
|
const std::string displayTitle{ utils::computeTrackDisplayTitle(track) };
|
||||||
|
|
||||||
oss
|
oss
|
||||||
<< "var params = {"
|
<< "var params = {"
|
||||||
@@ -252,7 +253,7 @@ namespace lms::ui
|
|||||||
<< " transcodingResource: \"" << transcodingResource << "\","
|
<< " transcodingResource: \"" << transcodingResource << "\","
|
||||||
<< " duration: " << std::chrono::duration_cast<std::chrono::duration<float>>(track->getDuration()).count() << ","
|
<< " duration: " << std::chrono::duration_cast<std::chrono::duration<float>>(track->getDuration()).count() << ","
|
||||||
<< " replayGain: " << replayGain << ","
|
<< " replayGain: " << replayGain << ","
|
||||||
<< " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\","
|
<< " title: \"" << core::stringUtils::jsEscape(displayTitle) << "\","
|
||||||
<< " artist: \"" << (!artistDisplayInfo.displayName.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
<< " artist: \"" << (!artistDisplayInfo.displayName.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
||||||
<< " release: \"" << (release ? core::stringUtils::jsEscape(release->getName()) : "") << "\",";
|
<< " release: \"" << (release ? core::stringUtils::jsEscape(release->getName()) : "") << "\",";
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ namespace lms::ui
|
|||||||
oss << jsRef() + ".mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
oss << jsRef() + ".mediaplayer.loadTrack(params, " << (play ? "true" : "false") << ")"; // true to autoplay
|
||||||
|
|
||||||
_title->setTextFormat(Wt::TextFormat::Plain);
|
_title->setTextFormat(Wt::TextFormat::Plain);
|
||||||
_title->setText(Wt::WString::fromUTF8(track->getName()));
|
_title->setText(Wt::WString::fromUTF8(displayTitle));
|
||||||
|
|
||||||
_artists->clear();
|
_artists->clear();
|
||||||
_artists->addWidget(utils::createArtistsAnchors(artistDisplayInfo));
|
_artists->addWidget(utils::createArtistsAnchors(artistDisplayInfo));
|
||||||
|
|||||||
+12
-10
@@ -368,7 +368,7 @@ namespace lms::ui
|
|||||||
entry->toggleStyleClass("Lms-entry-playing", selected);
|
entry->toggleStyleClass("Lms-entry-playing", selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::enqueueTracks(const std::vector<db::TrackId>& trackIds)
|
void PlayQueue::enqueueTracks(std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||||
@@ -425,35 +425,35 @@ namespace lms::ui
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::play(const std::vector<db::TrackId>& trackIds)
|
void PlayQueue::play(std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
playAtIndex(trackIds, 0);
|
playAtIndex(trackIds, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::playNext(const std::vector<db::TrackId>& trackIds)
|
void PlayQueue::playNext(std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
std::vector<db::TrackId> nextTracks{ getAndClearNextTracks() };
|
std::vector<db::TrackId> nextTracks{ getAndClearNextTracks() };
|
||||||
nextTracks.insert(std::cbegin(nextTracks), std::cbegin(trackIds), std::cend(trackIds));
|
nextTracks.insert(std::cbegin(nextTracks), std::cbegin(trackIds), std::cend(trackIds));
|
||||||
playOrAddLast(nextTracks);
|
playOrAddLast(nextTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::playShuffled(const std::vector<db::TrackId>& trackIds)
|
void PlayQueue::playShuffled(std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
clearTracks();
|
clearTracks();
|
||||||
std::vector<db::TrackId> shuffledTrackIds{ trackIds };
|
std::vector<db::TrackId> shuffledTrackIds{ std::cbegin(trackIds), std::cend(trackIds) };
|
||||||
core::random::shuffleContainer(shuffledTrackIds);
|
core::random::shuffleContainer(shuffledTrackIds);
|
||||||
enqueueTracks(shuffledTrackIds);
|
enqueueTracks(shuffledTrackIds);
|
||||||
loadTrack(0, true);
|
loadTrack(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::playOrAddLast(const std::vector<db::TrackId>& trackIds)
|
void PlayQueue::playOrAddLast(std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
enqueueTracks(trackIds);
|
enqueueTracks(trackIds);
|
||||||
if (!_isTrackSelected)
|
if (!_isTrackSelected)
|
||||||
loadTrack(0, true);
|
loadTrack(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueue::playAtIndex(const std::vector<db::TrackId>& trackIds, std::size_t index)
|
void PlayQueue::playAtIndex(std::span<const db::TrackId> trackIds, std::size_t index)
|
||||||
{
|
{
|
||||||
clearTracks();
|
clearTracks();
|
||||||
enqueueTracks(trackIds);
|
enqueueTracks(trackIds);
|
||||||
@@ -478,10 +478,12 @@ namespace lms::ui
|
|||||||
const auto track{ tracklistEntry->getTrack() };
|
const auto track{ tracklistEntry->getTrack() };
|
||||||
const db::TrackId trackId{ track->getId() };
|
const db::TrackId trackId{ track->getId() };
|
||||||
|
|
||||||
|
const std::string displayTitle{ utils::computeTrackDisplayTitle(track) };
|
||||||
|
|
||||||
Template* entry{ _entriesContainer->addNew<Template>(Wt::WString::tr("Lms.PlayQueue.template.entry")) };
|
Template* entry{ _entriesContainer->addNew<Template>(Wt::WString::tr("Lms.PlayQueue.template.entry")) };
|
||||||
entry->addFunction("id", &Wt::WTemplate::Functions::id);
|
entry->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
entry->bindString("name", Wt::WString::fromUTF8(displayTitle), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
const auto artists{ track->getArtistIds({ db::TrackArtistLinkType::Artist }) };
|
const auto artists{ track->getArtistIds({ db::TrackArtistLinkType::Artist }) };
|
||||||
if (!artists.empty())
|
if (!artists.empty())
|
||||||
@@ -520,7 +522,7 @@ namespace lms::ui
|
|||||||
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
||||||
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
|
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(displayTitle));
|
||||||
playBtn->clicked().connect([this, entry] {
|
playBtn->clicked().connect([this, entry] {
|
||||||
const std::optional<std::size_t> pos{ _entriesContainer->getIndexOf(*entry) };
|
const std::optional<std::size_t> pos{ _entriesContainer->getIndexOf(*entry) };
|
||||||
if (pos)
|
if (pos)
|
||||||
@@ -528,7 +530,7 @@ namespace lms::ui
|
|||||||
});
|
});
|
||||||
|
|
||||||
Wt::WPushButton* delBtn{ entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.template.delete-btn"), Wt::TextFormat::XHTML) };
|
Wt::WPushButton* delBtn{ entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.template.delete-btn"), Wt::TextFormat::XHTML) };
|
||||||
delBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.delete-item").arg(track->getName()));
|
delBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.delete-item").arg(displayTitle));
|
||||||
delBtn->setToolTip(Wt::WString::tr("Lms.delete"));
|
delBtn->setToolTip(Wt::WString::tr("Lms.delete"));
|
||||||
delBtn->clicked().connect([this, tracklistEntryId, entry] {
|
delBtn->clicked().connect([this, tracklistEntryId, entry] {
|
||||||
// Remove the entry n both the widget tree and the playqueue
|
// Remove the entry n both the widget tree and the playqueue
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
#include <Wt/WCheckBox.h>
|
#include <Wt/WCheckBox.h>
|
||||||
#include <Wt/WContainerWidget.h>
|
#include <Wt/WContainerWidget.h>
|
||||||
@@ -49,11 +50,11 @@ namespace lms::ui
|
|||||||
public:
|
public:
|
||||||
PlayQueue();
|
PlayQueue();
|
||||||
|
|
||||||
void play(const std::vector<db::TrackId>& trackIds);
|
void play(std::span<const db::TrackId> trackIds);
|
||||||
void playNext(const std::vector<db::TrackId>& trackIds);
|
void playNext(std::span<const db::TrackId> trackIds);
|
||||||
void playShuffled(const std::vector<db::TrackId>& trackIds);
|
void playShuffled(std::span<const db::TrackId> trackIds);
|
||||||
void playOrAddLast(const std::vector<db::TrackId>& trackIds); // play if queue empty, otherwise just add last
|
void playOrAddLast(std::span<const db::TrackId> trackIds); // play if queue empty, otherwise just add last
|
||||||
void playAtIndex(const std::vector<db::TrackId>& trackIds, std::size_t index);
|
void playAtIndex(std::span<const db::TrackId> trackIds, std::size_t index);
|
||||||
|
|
||||||
// play the next track in the queue
|
// play the next track in the queue
|
||||||
void playNext();
|
void playNext();
|
||||||
@@ -83,7 +84,7 @@ namespace lms::ui
|
|||||||
bool isFull() const;
|
bool isFull() const;
|
||||||
|
|
||||||
void clearTracks();
|
void clearTracks();
|
||||||
void enqueueTracks(const std::vector<db::TrackId>& trackIds);
|
void enqueueTracks(std::span<const db::TrackId> trackIds);
|
||||||
std::vector<db::TrackId> getAndClearNextTracks();
|
std::vector<db::TrackId> getAndClearNextTracks();
|
||||||
void addSome();
|
void addSome();
|
||||||
void addEntry(const db::ObjectPtr<db::TrackListEntry>& entry);
|
void addEntry(const db::ObjectPtr<db::TrackListEntry>& entry);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
#include "database/objects/Image.hpp"
|
#include "database/objects/Image.hpp"
|
||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/ReleaseArtistLink.hpp"
|
#include "database/objects/ReleaseArtistLink.hpp"
|
||||||
#include "database/objects/ScanSettings.hpp"
|
#include "database/objects/ScanSettings.hpp"
|
||||||
@@ -560,4 +561,26 @@ namespace lms::ui::utils
|
|||||||
{
|
{
|
||||||
LmsApp->doJavaScript("navigator.clipboard.writeText('" + core::stringUtils::jsEscape(text) + "').catch(function(){})");
|
LmsApp->doJavaScript("navigator.clipboard.writeText('" + core::stringUtils::jsEscape(text) + "').catch(function(){})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string computeTrackDisplayTitle(const db::ObjectPtr<db::Track>& track)
|
||||||
|
{
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
if (!movements.empty() && track->hasWork())
|
||||||
|
{
|
||||||
|
const db::Movement::pointer& movement{ movements.front() };
|
||||||
|
std::string result;
|
||||||
|
if (const auto n{ movement->getNumber() })
|
||||||
|
{
|
||||||
|
if (const std::string numeral{ core::stringUtils::toRomanNumeral(*n) }; !numeral.empty())
|
||||||
|
result += numeral + ". ";
|
||||||
|
}
|
||||||
|
|
||||||
|
result += movement->getName();
|
||||||
|
|
||||||
|
if (!result.empty())
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string{ track->getName() };
|
||||||
|
}
|
||||||
} // namespace lms::ui::utils
|
} // namespace lms::ui::utils
|
||||||
|
|||||||
@@ -103,4 +103,7 @@ namespace lms::ui::utils
|
|||||||
std::unique_ptr<Wt::WInteractWidget> createCopyright(std::string_view copyright, std::string_view copyrightURL);
|
std::unique_ptr<Wt::WInteractWidget> createCopyright(std::string_view copyright, std::string_view copyrightURL);
|
||||||
|
|
||||||
void copyToClipboard(std::string_view text);
|
void copyToClipboard(std::string_view text);
|
||||||
|
|
||||||
|
// Returns "i. MovementName" when track has work(s) + movement(s), otherwise track->getName().
|
||||||
|
std::string computeTrackDisplayTitle(const db::ObjectPtr<db::Track>& track);
|
||||||
} // namespace lms::ui::utils
|
} // namespace lms::ui::utils
|
||||||
|
|||||||
@@ -196,23 +196,27 @@ namespace lms::ui
|
|||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::Play, { _artistId });
|
db::ArtistId artists[]{ _artistId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::Play, artists);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, { _artistId });
|
db::ArtistId artists[]{ _artistId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, artists);
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, { _artistId });
|
db::ArtistId artists[]{ _artistId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, artists);
|
||||||
});
|
});
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { _artistId });
|
db::ArtistId artists[]{ _artistId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, artists);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (LmsApp->areDownloadsEnabled())
|
if (LmsApp->areDownloadsEnabled())
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include "explore/PlayQueueController.hpp"
|
#include "explore/PlayQueueController.hpp"
|
||||||
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
@@ -31,7 +30,7 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
std::vector<db::TrackId> getArtistsTracks(db::Session& session, const std::vector<db::ArtistId>& artistsId, const Filters& filters, std::size_t maxTrackCount)
|
std::vector<db::TrackId> getArtistsTracks(db::Session& session, std::span<const db::ArtistId> artistsId, const Filters& filters, std::size_t maxTrackCount)
|
||||||
{
|
{
|
||||||
assert(maxTrackCount);
|
assert(maxTrackCount);
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ namespace lms::ui
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount)
|
std::vector<db::TrackId> getReleasesTracks(db::Session& session, std::span<const db::ReleaseId> releasesId, const Filters& filters, std::size_t maxTrackCount)
|
||||||
{
|
{
|
||||||
assert(maxTrackCount);
|
assert(maxTrackCount);
|
||||||
|
|
||||||
@@ -87,25 +86,6 @@ namespace lms::ui
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<db::TrackId> getMediumTracks(db::Session& session, db::MediumId medium, const Filters& filters, std::size_t maxTrackCount)
|
|
||||||
{
|
|
||||||
assert(maxTrackCount);
|
|
||||||
|
|
||||||
std::vector<db::TrackId> res;
|
|
||||||
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
db::Track::FindParameters params;
|
|
||||||
params.setMedium(medium);
|
|
||||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
|
||||||
params.setFilters(filters.getDbFilters());
|
|
||||||
params.setRange(db::Range{ .offset = 0, .size = maxTrackCount });
|
|
||||||
|
|
||||||
const auto tracks{ db::Track::findIds(session, params) };
|
|
||||||
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
|
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
|
||||||
{
|
{
|
||||||
assert(maxTrackCount);
|
assert(maxTrackCount);
|
||||||
@@ -128,19 +108,19 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueueController::processCommand(Command command, const std::vector<db::ArtistId>& artistIds)
|
void PlayQueueController::processCommand(Command command, std::span<const db::ArtistId> artistIds)
|
||||||
{
|
{
|
||||||
const std::vector<db::TrackId> tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters, _maxTrackCountToEnqueue) };
|
const std::vector<db::TrackId> tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters, _maxTrackCountToEnqueue) };
|
||||||
processCommand(command, tracks);
|
processCommand(command, tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueueController::processCommand(Command command, const std::vector<db::ReleaseId>& releaseIds)
|
void PlayQueueController::processCommand(Command command, std::span<const db::ReleaseId> releaseIds)
|
||||||
{
|
{
|
||||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters, _maxTrackCountToEnqueue) };
|
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters, _maxTrackCountToEnqueue) };
|
||||||
processCommand(command, tracks);
|
processCommand(command, tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueueController::processCommand(Command command, const std::vector<db::TrackId>& trackIds)
|
void PlayQueueController::processCommand(Command command, std::span<const db::TrackId> trackIds)
|
||||||
{
|
{
|
||||||
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
// consider things are already filtered here, and _maxTrackCount honored playqueue side...
|
||||||
switch (command)
|
switch (command)
|
||||||
@@ -166,30 +146,8 @@ namespace lms::ui
|
|||||||
processCommand(command, tracks);
|
processCommand(command, tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayQueueController::processCommand(Command command, db::MediumId medium)
|
void PlayQueueController::playAtIndex(std::span<const db::TrackId> trackIds, std::size_t index)
|
||||||
{
|
{
|
||||||
const std::vector<db::TrackId> tracks{ getMediumTracks(LmsApp->getDbSession(), medium, _filters, _maxTrackCountToEnqueue) };
|
_playQueue.playAtIndex(trackIds, index);
|
||||||
processCommand(command, tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayQueueController::playTrackInRelease(db::TrackId trackId)
|
|
||||||
{
|
|
||||||
db::ReleaseId releaseId;
|
|
||||||
{
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
|
||||||
const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) };
|
|
||||||
if (!track || !track->getRelease())
|
|
||||||
return;
|
|
||||||
|
|
||||||
releaseId = track->getRelease()->getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<db::TrackId> tracks{ getReleasesTracks(LmsApp->getDbSession(), { releaseId }, _filters, _maxTrackCountToEnqueue) };
|
|
||||||
auto itTrack{ std::find(std::cbegin(tracks), std::cend(tracks), trackId) };
|
|
||||||
if (itTrack == std::cend(tracks))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const std::size_t index{ static_cast<std::size_t>(std::distance(std::cbegin(tracks), itTrack)) };
|
|
||||||
_playQueue.playAtIndex(tracks, index);
|
|
||||||
}
|
}
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "database/objects/ArtistId.hpp"
|
#include "database/objects/ArtistId.hpp"
|
||||||
#include "database/objects/MediumId.hpp"
|
|
||||||
#include "database/objects/ReleaseId.hpp"
|
#include "database/objects/ReleaseId.hpp"
|
||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
#include "database/objects/TrackListId.hpp"
|
#include "database/objects/TrackListId.hpp"
|
||||||
@@ -37,6 +37,9 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PlayQueueController(Filters& filters, PlayQueue& playQueue);
|
PlayQueueController(Filters& filters, PlayQueue& playQueue);
|
||||||
|
~PlayQueueController() = default;
|
||||||
|
PlayQueueController(const PlayQueueController&) = delete;
|
||||||
|
PlayQueueController& operator=(const PlayQueueController&) = delete;
|
||||||
|
|
||||||
enum class Command
|
enum class Command
|
||||||
{
|
{
|
||||||
@@ -46,13 +49,12 @@ namespace lms::ui
|
|||||||
PlayShuffled,
|
PlayShuffled,
|
||||||
};
|
};
|
||||||
|
|
||||||
void processCommand(Command command, const std::vector<db::ArtistId>& artists);
|
void processCommand(Command command, std::span<const db::ArtistId> artists);
|
||||||
void processCommand(Command command, const std::vector<db::ReleaseId>& releases);
|
void processCommand(Command command, std::span<const db::ReleaseId> releases);
|
||||||
void processCommand(Command command, const std::vector<db::TrackId>& tracks);
|
void processCommand(Command command, std::span<const db::TrackId> tracks);
|
||||||
|
|
||||||
void processCommand(Command command, db::MediumId medium);
|
|
||||||
void processCommand(Command command, db::TrackListId trackList);
|
void processCommand(Command command, db::TrackListId trackList);
|
||||||
void playTrackInRelease(db::TrackId track);
|
void playAtIndex(std::span<const db::TrackId> trackIds, std::size_t index);
|
||||||
|
|
||||||
void setMaxTrackCountToEnqueue(std::size_t maxTrackCount) { _maxTrackCountToEnqueue = maxTrackCount; }
|
void setMaxTrackCountToEnqueue(std::size_t maxTrackCount) { _maxTrackCountToEnqueue = maxTrackCount; }
|
||||||
|
|
||||||
|
|||||||
+325
-176
@@ -20,7 +20,9 @@
|
|||||||
#include "ReleaseView.hpp"
|
#include "ReleaseView.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
@@ -30,6 +32,7 @@
|
|||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
#include "core/Utils.hpp"
|
||||||
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
@@ -40,11 +43,13 @@
|
|||||||
#include "database/objects/Language.hpp"
|
#include "database/objects/Language.hpp"
|
||||||
#include "database/objects/Medium.hpp"
|
#include "database/objects/Medium.hpp"
|
||||||
#include "database/objects/Mood.hpp"
|
#include "database/objects/Mood.hpp"
|
||||||
|
#include "database/objects/Movement.hpp"
|
||||||
#include "database/objects/Release.hpp"
|
#include "database/objects/Release.hpp"
|
||||||
#include "database/objects/ScanSettings.hpp"
|
#include "database/objects/ScanSettings.hpp"
|
||||||
#include "database/objects/Track.hpp"
|
#include "database/objects/Track.hpp"
|
||||||
#include "database/objects/TrackArtistLink.hpp"
|
#include "database/objects/TrackArtistLink.hpp"
|
||||||
#include "database/objects/User.hpp"
|
#include "database/objects/User.hpp"
|
||||||
|
#include "database/objects/Work.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
@@ -197,11 +202,21 @@ namespace lms::ui
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate> createNoDiscContainer()
|
template<typename T, typename SameRunFunc, typename Visitor>
|
||||||
|
void visitRuns(std::span<const T> entries, SameRunFunc sameRun, Visitor visit)
|
||||||
{
|
{
|
||||||
return std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-nodisc"));
|
std::size_t startIndex{};
|
||||||
|
for (std::size_t i{ 1 }; i < entries.size(); ++i)
|
||||||
|
{
|
||||||
|
if (!sameRun(entries[i - 1], entries[i]))
|
||||||
|
{
|
||||||
|
visit(entries.subspan(startIndex, i - startIndex));
|
||||||
|
startIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!entries.empty())
|
||||||
|
visit(entries.subspan(startIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Release::Release(Filters& filters, PlayQueueController& playQueueController)
|
Release::Release(Filters& filters, PlayQueueController& playQueueController)
|
||||||
@@ -329,28 +344,33 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto discInfoList{ createDiscInfoList(release) };
|
||||||
|
_trackIds.clear();
|
||||||
|
for (const DiscInfo& disc : discInfoList)
|
||||||
|
appendTrackIds(_trackIds, disc);
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::Play, { _releaseId });
|
_playQueueController.processCommand(PlayQueueController::Command::Play, _trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, { _releaseId });
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, _trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, { _releaseId });
|
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, _trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"), Wt::TextFormat::Plain)
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this] {
|
.connect([this] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { _releaseId });
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, _trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (LmsApp->areDownloadsEnabled())
|
if (LmsApp->areDownloadsEnabled())
|
||||||
@@ -384,7 +404,7 @@ namespace lms::ui
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshDiscs(release);
|
refreshDiscs(release, discInfoList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::refreshArtwork(db::ArtworkId artworkId)
|
void Release::refreshArtwork(db::ArtworkId artworkId)
|
||||||
@@ -416,152 +436,286 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::refreshDiscs(const db::Release::pointer& release)
|
void Release::refreshDiscs(const db::ObjectPtr<db::Release>& release, std::span<const DiscInfo> discoInfoList)
|
||||||
{
|
{
|
||||||
Wt::WContainerWidget* discsContainer{ bindNew<Wt::WContainerWidget>("disc-container") };
|
Wt::WContainerWidget* discContainer{ bindNew<Wt::WContainerWidget>("disc-container") };
|
||||||
|
|
||||||
|
const DisplayOptions displayOptions{
|
||||||
|
.displayTrackArtists = shouldDisplayTrackArtists(release),
|
||||||
|
.showDiscHeaders = discoInfoList.size() > 1
|
||||||
|
|| release->getTotalDisc().value_or(0) > 1
|
||||||
|
|| std::any_of(std::cbegin(discoInfoList), std::cend(discoInfoList), [](const DiscInfo& discInfo) {
|
||||||
|
return !discInfo.medium->getName().empty() || discInfo.medium->getPreferredArtworkId().isValid();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const DiscInfo& discInfo : discoInfoList)
|
||||||
|
addDisc(discContainer, discInfo, displayOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Release::DiscInfo> Release::createDiscInfoList(const db::Release::pointer& release)
|
||||||
|
{
|
||||||
|
std::vector<Release::DiscInfo> discInfoList;
|
||||||
|
|
||||||
const std::vector<db::Medium::pointer> mediums{ release->getMediums() };
|
const std::vector<db::Medium::pointer> mediums{ release->getMediums() };
|
||||||
if (mediums.empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool displayTrackArtists{ shouldDisplayTrackArtists(release) };
|
|
||||||
const auto totalDisc{ release->getTotalDisc() };
|
|
||||||
|
|
||||||
const bool createDiscs{ mediums.size() > 1
|
|
||||||
|| !mediums[0]->getName().empty()
|
|
||||||
|| totalDisc > 1 };
|
|
||||||
|
|
||||||
|
discInfoList.reserve(mediums.size());
|
||||||
for (const db::Medium::pointer& medium : mediums)
|
for (const db::Medium::pointer& medium : mediums)
|
||||||
{
|
discInfoList.push_back(createDiscInfo(medium));
|
||||||
Wt::WTemplate* disc{ discsContainer->addWidget<Wt::WTemplate>(createDiscs ? createDisc(medium) : createNoDiscContainer()) };
|
|
||||||
|
|
||||||
Wt::WContainerWidget* tracksContainer{ disc->bindNew<Wt::WContainerWidget>("tracks") };
|
return discInfoList;
|
||||||
createTracks(tracksContainer, medium, displayTrackArtists);
|
}
|
||||||
|
|
||||||
|
Release::DiscInfo Release::createDiscInfo(const db::Medium::pointer& medium)
|
||||||
|
{
|
||||||
|
const std::vector<TrackInfo> tracks{ collectMediumTrackInfoList(medium) };
|
||||||
|
|
||||||
|
auto trackUseWork{ [](const TrackInfo& info) { return info.work && info.movement; } };
|
||||||
|
auto trackComputeWorkKey{ [&trackUseWork](const TrackInfo& info) { return trackUseWork(info) ? info.work->getId() : db::WorkId{}; } };
|
||||||
|
|
||||||
|
DiscInfo disc;
|
||||||
|
disc.medium = medium;
|
||||||
|
|
||||||
|
visitRuns<TrackInfo>(
|
||||||
|
tracks,
|
||||||
|
[&](const TrackInfo& a, const TrackInfo& b) { return trackComputeWorkKey(a) == trackComputeWorkKey(b); },
|
||||||
|
[&](std::span<const TrackInfo> entries) {
|
||||||
|
std::vector<TrackInfo> tracks(std::cbegin(entries), std::cend(entries));
|
||||||
|
|
||||||
|
if (trackUseWork(tracks.front()))
|
||||||
|
{
|
||||||
|
std::stable_sort(std::begin(tracks), std::end(tracks), [](const TrackInfo& a, const TrackInfo& b) {
|
||||||
|
// fallback on track-number order
|
||||||
|
return a.movement->getNumber().value_or(0) < b.movement->getNumber().value_or(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
disc.segments.push_back(WorkInfo{ .work = tracks.front().work, .tracks = std::move(tracks) });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
disc.segments.push_back(std::move(tracks));
|
||||||
|
} });
|
||||||
|
|
||||||
|
return disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release::appendTrackIds(std::vector<db::TrackId>& trackIds, const DiscInfo& disc)
|
||||||
|
{
|
||||||
|
for (const DiscInfo::Segment& segment : disc.segments)
|
||||||
|
{
|
||||||
|
std::visit(core::utils::overloads{
|
||||||
|
[&](const WorkInfo& work) {
|
||||||
|
appendTrackIds(trackIds, work.tracks);
|
||||||
|
},
|
||||||
|
[&](const TrackInfoList& tracks) {
|
||||||
|
appendTrackIds(trackIds, tracks);
|
||||||
|
} },
|
||||||
|
segment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::createTracks(Wt::WContainerWidget* tracksContainer, const db::Medium::pointer& medium, bool displayTrackArtists)
|
void Release::appendTrackIds(std::vector<db::TrackId>& trackIds, const TrackInfoList& tracks)
|
||||||
|
{
|
||||||
|
for (const TrackInfo& track : tracks)
|
||||||
|
trackIds.push_back(track.track->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release::addDisc(Wt::WContainerWidget* container, const DiscInfo& discInfo, DisplayOptions displayOptions)
|
||||||
|
{
|
||||||
|
if (displayOptions.showDiscHeaders)
|
||||||
|
{
|
||||||
|
Wt::WString discTitle;
|
||||||
|
if (discInfo.medium->getName().empty())
|
||||||
|
discTitle = Wt::WString::tr("Lms.Explore.Release.disc").arg(discInfo.medium->getPosition().value_or(1));
|
||||||
|
else
|
||||||
|
discTitle = Wt::WString::fromUTF8(std::string{ discInfo.medium->getName() });
|
||||||
|
|
||||||
|
std::vector<db::TrackId> trackIds;
|
||||||
|
appendTrackIds(trackIds, discInfo);
|
||||||
|
container = addSegment(container, discTitle, discInfo.medium->getPreferredArtworkId(), trackIds, "Lms.Explore.Release.template.segment-disc");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const DiscInfo::Segment& segment : discInfo.segments)
|
||||||
|
{
|
||||||
|
std::visit(core::utils::overloads{
|
||||||
|
[&](const WorkInfo& work) {
|
||||||
|
std::vector<db::TrackId> trackIds;
|
||||||
|
appendTrackIds(trackIds, work.tracks);
|
||||||
|
|
||||||
|
Wt::WContainerWidget* workContainer{ addSegment(container, Wt::WString::fromUTF8(std::string{ work.work->getName() }), db::ArtworkId{}, trackIds, "Lms.Explore.Release.template.segment-work") };
|
||||||
|
addTrackEntries(workContainer, work.tracks, displayOptions.displayTrackArtists);
|
||||||
|
},
|
||||||
|
[&](const TrackInfoList& tracks) {
|
||||||
|
std::vector<db::TrackId> trackIds;
|
||||||
|
appendTrackIds(trackIds, tracks);
|
||||||
|
|
||||||
|
// A run of tracks with no work header
|
||||||
|
auto* flat{ container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.segment-flat")) };
|
||||||
|
addTrackEntries(flat->bindNew<Wt::WContainerWidget>("tracks"), tracks, displayOptions.displayTrackArtists);
|
||||||
|
} },
|
||||||
|
segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Release::TrackInfoList Release::collectMediumTrackInfoList(const db::Medium::pointer& medium)
|
||||||
{
|
{
|
||||||
db::Track::FindParameters params;
|
db::Track::FindParameters params;
|
||||||
params.setMedium(medium->getId());
|
params.setMedium(medium->getId());
|
||||||
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
params.setSortMethod(db::TrackSortMethod::TrackNumber);
|
||||||
|
|
||||||
|
std::vector<TrackInfo> tracks;
|
||||||
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
|
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
|
||||||
const db::TrackId trackId{ track->getId() };
|
const auto works{ track->getWorks() };
|
||||||
|
const auto movements{ track->getMovements() };
|
||||||
|
|
||||||
Template* entry{ tracksContainer->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry")) };
|
tracks.push_back(TrackInfo{
|
||||||
entry->addFunction("id", &Wt::WTemplate::Functions::id);
|
.track = track,
|
||||||
|
.work = works.empty() ? db::Work::pointer{} : works.front(),
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
.movement = movements.empty() ? db::Movement::pointer{} : movements.front(),
|
||||||
|
|
||||||
if (displayTrackArtists)
|
|
||||||
{
|
|
||||||
const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
|
|
||||||
if (!artistDisplayInfo.entries.empty())
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-artists", true);
|
|
||||||
entry->bindWidget("artists", utils::createArtistsAnchors(artistDisplayInfo));
|
|
||||||
entry->bindWidget("artists-md", utils::createArtistsAnchors(artistDisplayInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillTrackArtistLinks(entry, track);
|
|
||||||
|
|
||||||
auto trackNumber{ track->getTrackNumber() };
|
|
||||||
if (trackNumber)
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-track-number", true);
|
|
||||||
entry->bindInt("track-number", *trackNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
|
||||||
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
|
|
||||||
playBtn->clicked().connect([this, trackId] {
|
|
||||||
_playQueueController.playTrackInRelease(trackId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
|
||||||
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
|
||||||
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
|
|
||||||
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
|
||||||
->clicked()
|
|
||||||
.connect([this, trackId] {
|
|
||||||
_playQueueController.playTrackInRelease(trackId);
|
|
||||||
});
|
|
||||||
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
|
||||||
->clicked()
|
|
||||||
.connect([this, trackId] {
|
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, { trackId });
|
|
||||||
});
|
|
||||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
|
||||||
->clicked()
|
|
||||||
.connect([this, trackId] {
|
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { trackId });
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
|
|
||||||
|
|
||||||
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
|
|
||||||
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
|
|
||||||
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(track->getName()));
|
|
||||||
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
|
|
||||||
|
|
||||||
auto toggle{ [=] {
|
|
||||||
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
|
||||||
|
|
||||||
if (isStarred())
|
|
||||||
{
|
|
||||||
core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
|
|
||||||
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star"));
|
|
||||||
starBtn->setText(Wt::WString::tr("Lms.template.star-btn"));
|
|
||||||
starBtn->setAttributeValue("aria-pressed", "false");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
|
|
||||||
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
|
||||||
starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn"));
|
|
||||||
starBtn->setAttributeValue("aria-pressed", "true");
|
|
||||||
}
|
|
||||||
} };
|
|
||||||
|
|
||||||
starMenuEntry->clicked().connect([=] { toggle(); });
|
|
||||||
starBtn->clicked().connect([=] { toggle(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LmsApp->areDownloadsEnabled())
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-download", true);
|
|
||||||
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
|
||||||
->setLink(Wt::WLink{ std::make_unique<DownloadTrackResource>(trackId) });
|
|
||||||
}
|
|
||||||
|
|
||||||
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
|
|
||||||
->clicked()
|
|
||||||
.connect([this, trackId] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
|
|
||||||
|
|
||||||
if (track->hasLyrics())
|
|
||||||
{
|
|
||||||
entry->setCondition("if-has-lyrics", true);
|
|
||||||
entry->bindNew<Wt::WPushButton>("track-lyrics", Wt::WString::tr("Lms.Explore.track-lyrics"))
|
|
||||||
->clicked()
|
|
||||||
.connect([trackId] { TrackListHelpers::showTrackLyricsModal(trackId); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
|
||||||
|
|
||||||
LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=](db::TrackId loadedTrackId) {
|
|
||||||
entry->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (auto trackIdLoaded{ LmsApp->getMediaPlayer().getTrackLoaded() })
|
|
||||||
{
|
|
||||||
entry->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
entry->removeStyleClass("Lms-entry-playing");
|
|
||||||
});
|
});
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release::addTrackEntries(Wt::WContainerWidget* container, std::span<const TrackInfo> tracks, bool displayTrackArtists)
|
||||||
|
{
|
||||||
|
for (const TrackInfo& trackInfo : tracks)
|
||||||
|
addTrackEntry(container, trackInfo, displayTrackArtists);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Release::addTrackEntry(Wt::WContainerWidget* container, const TrackInfo& trackInfo, bool displayTrackArtists)
|
||||||
|
{
|
||||||
|
const db::Track::pointer& track{ trackInfo.track };
|
||||||
|
const db::TrackId trackId{ track->getId() };
|
||||||
|
|
||||||
|
Template* entry{ container->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.track")) };
|
||||||
|
entry->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
|
const std::string title{ (trackInfo.movement && !trackInfo.movement->getName().empty()) ? trackInfo.movement->getName() : track->getName() };
|
||||||
|
entry->bindString("name", Wt::WString::fromUTF8(title), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
|
if (displayTrackArtists)
|
||||||
|
{
|
||||||
|
const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
|
||||||
|
if (!artistDisplayInfo.entries.empty())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-artists", true);
|
||||||
|
entry->bindWidget("artists", utils::createArtistsAnchors(artistDisplayInfo));
|
||||||
|
entry->bindWidget("artists-md", utils::createArtistsAnchors(artistDisplayInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fillTrackArtistLinks(entry, track);
|
||||||
|
|
||||||
|
if (trackInfo.movement && trackInfo.movement->getNumber())
|
||||||
|
{
|
||||||
|
const std::size_t number{ *trackInfo.movement->getNumber() };
|
||||||
|
entry->setCondition("if-has-position", true);
|
||||||
|
if (const std::string numeral{ core::stringUtils::toRomanNumeral(number) }; !numeral.empty())
|
||||||
|
entry->bindString("position", Wt::WString::fromUTF8(numeral), Wt::TextFormat::Plain);
|
||||||
|
else
|
||||||
|
entry->bindInt("position", static_cast<int>(number));
|
||||||
|
}
|
||||||
|
else if (const auto n{ track->getTrackNumber() })
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-position", true);
|
||||||
|
entry->bindInt("position", static_cast<int>(*n));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto playTrack{ [this, trackId] {
|
||||||
|
const auto it{ std::find(std::cbegin(_trackIds), std::cend(_trackIds), trackId) };
|
||||||
|
if (it == std::cend(_trackIds))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_playQueueController.playAtIndex(_trackIds, static_cast<std::size_t>(std::distance(std::cbegin(_trackIds), it)));
|
||||||
|
} };
|
||||||
|
|
||||||
|
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
||||||
|
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(title));
|
||||||
|
playBtn->clicked().connect(playTrack);
|
||||||
|
|
||||||
|
{
|
||||||
|
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
||||||
|
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
|
||||||
|
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
||||||
|
->clicked()
|
||||||
|
.connect(playTrack);
|
||||||
|
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
||||||
|
->clicked()
|
||||||
|
.connect([this, trackId] {
|
||||||
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, trackIds);
|
||||||
|
});
|
||||||
|
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||||
|
->clicked()
|
||||||
|
.connect([this, trackId] {
|
||||||
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, trackIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
|
||||||
|
|
||||||
|
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
|
||||||
|
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
|
||||||
|
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(title));
|
||||||
|
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
|
||||||
|
|
||||||
|
auto toggle{ [=] {
|
||||||
|
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||||
|
|
||||||
|
if (isStarred())
|
||||||
|
{
|
||||||
|
core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||||
|
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
|
starBtn->setText(Wt::WString::tr("Lms.template.star-btn"));
|
||||||
|
starBtn->setAttributeValue("aria-pressed", "false");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
|
||||||
|
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
|
starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn"));
|
||||||
|
starBtn->setAttributeValue("aria-pressed", "true");
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
|
||||||
|
starMenuEntry->clicked().connect([=] { toggle(); });
|
||||||
|
starBtn->clicked().connect([=] { toggle(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LmsApp->areDownloadsEnabled())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-download", true);
|
||||||
|
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||||
|
->setLink(Wt::WLink{ std::make_unique<DownloadTrackResource>(trackId) });
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
|
||||||
|
->clicked()
|
||||||
|
.connect([this, trackId] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
|
||||||
|
|
||||||
|
if (track->hasLyrics())
|
||||||
|
{
|
||||||
|
entry->setCondition("if-has-lyrics", true);
|
||||||
|
entry->bindNew<Wt::WPushButton>("track-lyrics", Wt::WString::tr("Lms.Explore.track-lyrics"))
|
||||||
|
->clicked()
|
||||||
|
.connect([trackId] { TrackListHelpers::showTrackLyricsModal(trackId); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
|
LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=](db::TrackId loadedTrackId) {
|
||||||
|
entry->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auto trackIdLoaded{ LmsApp->getMediaPlayer().getTrackLoaded() })
|
||||||
|
{
|
||||||
|
entry->toggleStyleClass("Lms-entry-playing", *trackIdLoaded == trackId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
entry->removeStyleClass("Lms-entry-playing");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::refreshCopyright(const db::Release::pointer& release)
|
void Release::refreshCopyright(const db::Release::pointer& release)
|
||||||
@@ -626,7 +780,7 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::refreshRelatedReleases(const std::vector<db::ReleaseId>& similarReleaseIds)
|
void Release::refreshRelatedReleases(std::span<const db::ReleaseId> similarReleaseIds)
|
||||||
{
|
{
|
||||||
if (similarReleaseIds.empty())
|
if (similarReleaseIds.empty())
|
||||||
return;
|
return;
|
||||||
@@ -644,63 +798,58 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate> Release::createDisc(const db::Medium::pointer& medium)
|
Wt::WContainerWidget* Release::addSegment(Wt::WContainerWidget* container, const Wt::WString& title, db::ArtworkId artworkId, std::span<const db::TrackId> trackIds, const char* segmentTemplate)
|
||||||
{
|
{
|
||||||
const db::MediumId mediumId{ medium->getId() };
|
const std::vector<db::TrackId> ids{ std::cbegin(trackIds), std::cend(trackIds) };
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate> disc{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
|
auto header{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.segment-header")) };
|
||||||
disc->addFunction("id", &Wt::WTemplate::Functions::id);
|
header->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
if (db::ArtworkId artworkId{ medium->getPreferredArtworkId() }; artworkId.isValid())
|
if (artworkId.isValid())
|
||||||
{
|
{
|
||||||
auto image{ utils::createArtworkImage(artworkId, ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Small) };
|
auto image{ utils::createArtworkImage(artworkId, ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Small) };
|
||||||
|
header->setCondition("if-has-artwork", true);
|
||||||
disc->setCondition("if-has-artwork", true);
|
|
||||||
|
|
||||||
image->addStyleClass("Lms-cover-track rounded"); // HACK
|
image->addStyleClass("Lms-cover-track rounded"); // HACK
|
||||||
image->clicked().connect([artworkId] {
|
image->clicked().connect([artworkId] {
|
||||||
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::DefaultArtworkType::Release) });
|
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::DefaultArtworkType::Release) });
|
||||||
});
|
});
|
||||||
disc->bindWidget<Wt::WImage>("artwork", std::move(image));
|
header->bindWidget<Wt::WImage>("artwork", std::move(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WString discTitle;
|
header->bindNew<Wt::WText>("title", title, Wt::TextFormat::Plain);
|
||||||
if (medium->getName().empty())
|
|
||||||
discTitle = Wt::WString::tr("Lms.Explore.Release.disc").arg(medium->getPosition() ? *medium->getPosition() : 1 /* TODO */);
|
|
||||||
else
|
|
||||||
discTitle = Wt::WString::fromUTF8(std::string{ medium->getName() });
|
|
||||||
disc->bindNew<Wt::WText>("disc-title", discTitle, Wt::TextFormat::Plain);
|
|
||||||
|
|
||||||
Wt::WPushButton* playBtn{ disc->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
Wt::WPushButton* playBtn{ header->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
||||||
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(discTitle));
|
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(title));
|
||||||
playBtn->clicked().connect([this, mediumId] {
|
playBtn->clicked().connect([this, ids] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::Play, mediumId);
|
_playQueueController.processCommand(PlayQueueController::Command::Play, ids);
|
||||||
});
|
});
|
||||||
disc->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
header->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
||||||
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
|
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
|
||||||
;
|
header->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
||||||
disc->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this, mediumId] {
|
.connect([this, ids] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::Play, mediumId);
|
_playQueueController.processCommand(PlayQueueController::Command::Play, ids);
|
||||||
});
|
});
|
||||||
disc->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
header->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([this, mediumId] {
|
.connect([this, ids] {
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, mediumId);
|
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, ids);
|
||||||
|
});
|
||||||
|
header->bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
||||||
|
->clicked()
|
||||||
|
.connect([this, ids] {
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, ids);
|
||||||
|
});
|
||||||
|
header->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||||
|
->clicked()
|
||||||
|
.connect([this, ids] {
|
||||||
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, ids);
|
||||||
});
|
});
|
||||||
|
|
||||||
disc->bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
|
Template* segment{ container->addNew<Template>(Wt::WString::tr(segmentTemplate)) };
|
||||||
->clicked()
|
segment->bindWidget("header", std::move(header));
|
||||||
.connect([this, mediumId] {
|
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, mediumId);
|
|
||||||
});
|
|
||||||
disc->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
|
||||||
->clicked()
|
|
||||||
.connect([this, mediumId] {
|
|
||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, mediumId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return disc;
|
return segment->bindNew<Wt::WContainerWidget>("tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -19,16 +19,24 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/objects/ArtworkId.hpp"
|
#include "database/objects/ArtworkId.hpp"
|
||||||
#include "database/objects/ReleaseId.hpp"
|
#include "database/objects/ReleaseId.hpp"
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Medium;
|
class Medium;
|
||||||
|
class Movement;
|
||||||
class Release;
|
class Release;
|
||||||
|
class Track;
|
||||||
|
class Work;
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
@@ -45,17 +53,54 @@ namespace lms::ui
|
|||||||
void refreshView();
|
void refreshView();
|
||||||
void refreshArtwork(db::ArtworkId artworkId);
|
void refreshArtwork(db::ArtworkId artworkId);
|
||||||
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
|
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshDiscs(const db::ObjectPtr<db::Release>& release);
|
|
||||||
void createTracks(Wt::WContainerWidget* tracksContainer, const db::ObjectPtr<db::Medium>& medium, bool displayTrackArtists);
|
|
||||||
void refreshCopyright(const db::ObjectPtr<db::Release>& release);
|
void refreshCopyright(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshLinks(const db::ObjectPtr<db::Release>& release);
|
void refreshLinks(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshOtherVersions(const db::ObjectPtr<db::Release>& release);
|
void refreshOtherVersions(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshRelatedReleases(const std::vector<db::ReleaseId>& similarReleaseIds);
|
void refreshRelatedReleases(std::span<const db::ReleaseId> similarReleaseIds);
|
||||||
|
|
||||||
std::unique_ptr<Wt::WTemplate> createDisc(const db::ObjectPtr<db::Medium>& medium);
|
struct TrackInfo
|
||||||
|
{
|
||||||
|
db::ObjectPtr<db::Track> track;
|
||||||
|
db::ObjectPtr<db::Work> work; // first work, if any
|
||||||
|
db::ObjectPtr<db::Movement> movement; // first movement, if any
|
||||||
|
};
|
||||||
|
|
||||||
|
using TrackInfoList = std::vector<TrackInfo>;
|
||||||
|
struct WorkInfo
|
||||||
|
{
|
||||||
|
db::ObjectPtr<db::Work> work;
|
||||||
|
TrackInfoList tracks;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DiscInfo
|
||||||
|
{
|
||||||
|
db::ObjectPtr<db::Medium> medium;
|
||||||
|
using Segment = std::variant<WorkInfo, TrackInfoList>;
|
||||||
|
std::vector<Segment> segments;
|
||||||
|
};
|
||||||
|
void refreshDiscs(const db::ObjectPtr<db::Release>& release, std::span<const DiscInfo> discoInfoList);
|
||||||
|
static std::vector<DiscInfo> createDiscInfoList(const db::ObjectPtr<db::Release>& release);
|
||||||
|
static DiscInfo createDiscInfo(const db::ObjectPtr<db::Medium>& medium);
|
||||||
|
|
||||||
|
static void appendTrackIds(std::vector<db::TrackId>& trackIds, const DiscInfo& disc);
|
||||||
|
static void appendTrackIds(std::vector<db::TrackId>& trackIds, const TrackInfoList& tracks);
|
||||||
|
|
||||||
|
struct DisplayOptions
|
||||||
|
{
|
||||||
|
bool displayTrackArtists{};
|
||||||
|
bool showDiscHeaders{};
|
||||||
|
};
|
||||||
|
void addDisc(Wt::WContainerWidget* container, const DiscInfo& discInfo, DisplayOptions displayOptions);
|
||||||
|
|
||||||
|
static TrackInfoList collectMediumTrackInfoList(const db::ObjectPtr<db::Medium>& medium);
|
||||||
|
|
||||||
|
Wt::WContainerWidget* addSegment(Wt::WContainerWidget* container, const Wt::WString& title, db::ArtworkId artworkId, std::span<const db::TrackId> trackIds, const char* segmentTemplate);
|
||||||
|
void addTrackEntries(Wt::WContainerWidget* container, std::span<const TrackInfo> tracks, bool displayTrackArtists);
|
||||||
|
void addTrackEntry(Wt::WContainerWidget* container, const TrackInfo& trackInfo, bool displayTrackArtists);
|
||||||
|
|
||||||
Filters& _filters;
|
Filters& _filters;
|
||||||
PlayQueueController& _playQueueController;
|
PlayQueueController& _playQueueController;
|
||||||
db::ReleaseId _releaseId;
|
db::ReleaseId _releaseId;
|
||||||
|
std::vector<db::TrackId> _trackIds; // ordered as displayed, spans all discs/works/movements
|
||||||
};
|
};
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ namespace lms::ui::TrackListHelpers
|
|||||||
auto entry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry")) };
|
auto entry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry")) };
|
||||||
auto* entryPtr{ entry.get() };
|
auto* entryPtr{ entry.get() };
|
||||||
|
|
||||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
const std::string displayTitle{ utils::computeTrackDisplayTitle(track) };
|
||||||
|
entry->bindString("name", Wt::WString::fromUTF8(displayTitle), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
const db::Release::pointer release{ track->getRelease() };
|
const db::Release::pointer release{ track->getRelease() };
|
||||||
const db::TrackId trackId{ track->getId() };
|
const db::TrackId trackId{ track->getId() };
|
||||||
@@ -269,9 +270,10 @@ namespace lms::ui::TrackListHelpers
|
|||||||
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
|
||||||
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
|
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(displayTitle));
|
||||||
playBtn->clicked().connect([trackId, &playQueueController] {
|
playBtn->clicked().connect([trackId, &playQueueController] {
|
||||||
playQueueController.processCommand(PlayQueueController::Command::Play, { trackId });
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
playQueueController.processCommand(PlayQueueController::Command::Play, trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
|
||||||
@@ -280,24 +282,27 @@ namespace lms::ui::TrackListHelpers
|
|||||||
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([trackId, &playQueueController] {
|
.connect([trackId, &playQueueController] {
|
||||||
playQueueController.processCommand(PlayQueueController::Command::Play, { trackId });
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
playQueueController.processCommand(PlayQueueController::Command::Play, trackIds);
|
||||||
});
|
});
|
||||||
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
entry->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([=, &playQueueController] {
|
.connect([=, &playQueueController] {
|
||||||
playQueueController.processCommand(PlayQueueController::Command::PlayNext, { trackId });
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
playQueueController.processCommand(PlayQueueController::Command::PlayNext, trackIds);
|
||||||
});
|
});
|
||||||
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
entry->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
|
||||||
->clicked()
|
->clicked()
|
||||||
.connect([=, &playQueueController] {
|
.connect([=, &playQueueController] {
|
||||||
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { trackId });
|
db::TrackId trackIds[]{ trackId };
|
||||||
|
playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, trackIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
|
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
|
||||||
|
|
||||||
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
|
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
|
||||||
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(track->getName()));
|
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(displayTitle));
|
||||||
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
|
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
|
||||||
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
|
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user