From a1001b296ad4d77265711e37f978f23e320e5af1 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 10 Jan 2024 23:38:48 +0100 Subject: [PATCH 1/4] OpenSubsonic: changed the originalReleaseDate field format, fixes #400 --- src/libs/database/impl/Migration.cpp | 11 ++ src/libs/database/impl/Migration.hpp | 2 +- src/libs/database/impl/Release.cpp | 49 ++++-- src/libs/database/impl/Track.cpp | 12 +- src/libs/database/impl/Types.cpp | 43 +++-- .../database/include/database/Release.hpp | 9 +- src/libs/database/include/database/Track.hpp | 12 +- src/libs/database/include/database/Types.hpp | 4 +- src/libs/database/test/Release.cpp | 57 +++++- src/libs/database/test/Track.cpp | 25 ++- src/libs/metadata/impl/TagLibParser.cpp | 28 +-- src/libs/metadata/impl/Utils.cpp | 14 +- .../metadata/include/metadata/IParser.hpp | 2 + src/libs/metadata/test/Utils.cpp | 164 +++++++++--------- .../scanner/impl/ScanStepScanFiles.cpp | 8 +- src/libs/subsonic/CMakeLists.txt | 1 + src/libs/subsonic/impl/ProtocolVersion.hpp | 2 +- src/libs/subsonic/impl/responses/Album.cpp | 11 +- src/libs/subsonic/impl/responses/ItemDate.cpp | 43 +++++ src/libs/subsonic/impl/responses/ItemDate.hpp | 28 +++ src/libs/utils/test/String.cpp | 15 +- src/lms/ui/explore/ReleaseHelpers.cpp | 12 +- src/lms/ui/explore/ReleaseHelpers.hpp | 2 +- src/lms/ui/explore/ReleaseView.cpp | 2 +- src/lms/ui/resource/DownloadResource.cpp | 4 +- src/tools/metadata/LmsMetadata.cpp | 4 + 26 files changed, 390 insertions(+), 174 deletions(-) create mode 100644 src/libs/subsonic/impl/responses/ItemDate.cpp create mode 100644 src/libs/subsonic/impl/responses/ItemDate.hpp diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 9eb6ec31..3cc5bcfe 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -299,6 +299,16 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); } + void migrateFromV49(Session& session) + { + // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) + session.getDboSession().execute("ALTER TABLE track ADD year INTEGER"); + session.getDboSession().execute("ALTER TABLE track ADD original_year INTEGER"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -326,6 +336,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {46, migrateFromV46}, {47, migrateFromV47}, {48, migrateFromV48}, + {49, migrateFromV49}, }; { diff --git a/src/libs/database/impl/Migration.hpp b/src/libs/database/impl/Migration.hpp index 7156de9d..2e4b5b5d 100644 --- a/src/libs/database/impl/Migration.hpp +++ b/src/libs/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION{ 49 }; + static constexpr Version LMS_DATABASE_VERSION{ 50 }; class VersionInfo { public: diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index f56b028e..dbfcbfe0 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -66,8 +66,8 @@ namespace Database if (params.dateRange) { - query.where("t.date >= ?").bind(params.dateRange->begin); - query.where("t.date <= ?").bind(params.dateRange->end); + query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) >= ?").bind(params.dateRange->begin); + query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) <= ?").bind(params.dateRange->end); } for (std::string_view keyword : params.keywords) @@ -170,13 +170,13 @@ namespace Database query.orderBy("t.file_last_write DESC"); break; case ReleaseSortMethod::Date: - query.orderBy("t.date, r.name COLLATE NOCASE"); + query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)), r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDate: - query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE"); + query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE"); break; case ReleaseSortMethod::OriginalDateDesc: - query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE"); + query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)) DESC, r.name COLLATE NOCASE"); break; case ReleaseSortMethod::StarredDateDesc: assert(params.starringUser.isValid()); @@ -359,17 +359,17 @@ namespace Database return discs; } - Wt::WDate Release::getReleaseDate() const + Wt::WDate Release::getDate() const { - return getReleaseDate(false); + return getDate(false); } - Wt::WDate Release::getOriginalReleaseDate() const + Wt::WDate Release::getOriginalDate() const { - return getReleaseDate(true); + return getDate(true); } - Wt::WDate Release::getReleaseDate(bool original) const + Wt::WDate Release::getDate(bool original) const { assert(session()); @@ -388,6 +388,35 @@ namespace Database return dates.front(); } + + std::optional Release::getYear() const + { + return getYear(false); + } + + std::optional Release::getOriginalYear() const + { + return getYear(true); + } + + std::optional Release::getYear(bool original) const + { + assert(session()); + + const char* field{ original ? "original_year" : "year" }; + + auto years{ session()->query>( + std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") + .where("r.id = ?").bind(getId()) + .groupBy(field) + .resultList() }; + + // various years => invalid years + if (years.empty() || years.size() > 1) + return std::nullopt; + + return years.front(); + } std::optional Release::getCopyright() const { diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 68124631..55fdf722 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -158,7 +158,7 @@ namespace Database query.orderBy("t.name COLLATE NOCASE"); break; case TrackSortMethod::DateDescAndRelease: - query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number"); + query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC,t.release_id,t.disc_number,t.track_number"); break; case TrackSortMethod::Release: query.orderBy("t.disc_number,t.track_number"); @@ -382,16 +382,6 @@ namespace Database _clusters.insert(getDboPtr(cluster)); } - std::optional Track::getYear() const - { - return (_date.isValid() ? std::make_optional(_date.year()) : std::nullopt); - } - - std::optional Track::getOriginalYear() const - { - return (_originalDate.isValid() ? std::make_optional(_originalDate.year()) : std::nullopt); - } - std::optional Track::getCopyright() const { return _copyright != "" ? std::make_optional(_copyright) : std::nullopt; diff --git a/src/libs/database/impl/Types.cpp b/src/libs/database/impl/Types.cpp index f3674083..456955cc 100644 --- a/src/libs/database/impl/Types.cpp +++ b/src/libs/database/impl/Types.cpp @@ -23,30 +23,29 @@ namespace Database { - static const std::set allowedAudioBitrates - { - 64000, - 96000, - 128000, - 192000, - 320000, - }; + static const std::set allowedAudioBitrates + { + 64000, + 96000, + 128000, + 192000, + 320000, + }; - void visitAllowedAudioBitrates(std::function func) - { - for (Bitrate bitrate : allowedAudioBitrates) - func(bitrate); - } + void visitAllowedAudioBitrates(std::function func) + { + for (Bitrate bitrate : allowedAudioBitrates) + func(bitrate); + } - bool isAudioBitrateAllowed(Bitrate bitrate) - { - return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); - } + bool isAudioBitrateAllowed(Bitrate bitrate) + { + return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); + } - DateRange - DateRange::fromYearRange(int from, int to) - { - return DateRange {{from, 1, 1}, {to, 12, 31}}; - } + DateRange DateRange::fromYearRange(int from, int to) + { + return DateRange{ from, to }; + } } diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index b91c4ad4..c79addd0 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -132,8 +132,10 @@ namespace Database std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const; // Utility functions (if all tracks have the same values, which is legit to not be the case) - Wt::WDate getReleaseDate() const; - Wt::WDate getOriginalReleaseDate() const; + Wt::WDate getDate() const; + std::optional getYear() const; + Wt::WDate getOriginalDate() const; + std::optional getOriginalYear() const; std::optional getCopyright() const; std::optional getCopyrightURL() const; std::size_t getMeanBitrate() const; @@ -181,7 +183,8 @@ namespace Database Release(const std::string& name, const std::optional& MBID = {}); static pointer create(Session& session, const std::string& name, const std::optional& MBID = {}); - Wt::WDate getReleaseDate(bool original) const; + Wt::WDate getDate(bool original) const; + std::optional getYear(bool original) const; static constexpr std::size_t _maxNameLength{ 128 }; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 3fed9eb7..3de943a4 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -132,7 +132,9 @@ namespace Database { void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } void setDate(const Wt::WDate& date) { _date = date; } + void setYear(std::optional year) { _year = year; } void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } + void setOriginalYear(std::optional year) { _originalYear = year; } void setHasCover(bool hasCover) { _hasCover = hasCover; } void setTrackMBID(const std::optional& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setRecordingMBID(const std::optional& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } @@ -156,8 +158,10 @@ namespace Database { std::chrono::milliseconds getDuration() const { return _duration; } std::size_t getBitrate() const { return _bitrate; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } - std::optional getYear() const; - std::optional getOriginalYear() const; + const Wt::WDate& getDate() const { return _date; } + std::optional getYear() const { return _year; } + const Wt::WDate& getOriginalDate() const { return _originalDate; } + std::optional getOriginalYear() const { return _originalYear; }; Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } Wt::WDateTime getAddedTime() const { return _fileAdded; } bool hasCover() const { return _hasCover; } @@ -190,7 +194,9 @@ namespace Database { Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _bitrate, "bitrate"); Wt::Dbo::field(a, _date, "date"); + Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _originalDate, "original_date"); + Wt::Dbo::field(a, _originalYear, "original_year"); Wt::Dbo::field(a, _filePath, "file_path"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileAdded, "file_added"); @@ -225,7 +231,9 @@ namespace Database { std::chrono::duration _duration{}; int _bitrate; // in bps Wt::WDate _date; + std::optional _year; Wt::WDate _originalDate; + std::optional _originalYear; std::string _filePath; Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileAdded; diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 14d96dee..a992f6bd 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -93,8 +93,8 @@ namespace Database struct DateRange { - Wt::WDate begin; - Wt::WDate end; + int begin; + int end; static DateRange fromYearRange(int from, int to); }; diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 52b736fd..59e84ec3 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -402,8 +402,61 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) track1A.get().modify()->setOriginalDate(release1OriginalDate); track1B.get().modify()->setOriginalDate(release1OriginalDate); - EXPECT_EQ(release1.get()->getReleaseDate(), release1Date); - EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate); + EXPECT_EQ(release1.get()->getDate(), release1Date); + EXPECT_EQ(release1.get()->getOriginalDate(), release1OriginalDate); + } + + { + auto transaction{ session.createReadTransaction() }; + + auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000))) }; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); + + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994))); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); + + releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993))); + ASSERT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, MultiTracksSingleReleaseYear) +{ + ScopedRelease release1{ session, "MyRelease1" }; + ScopedRelease release2{ session, "MyRelease2" }; + const int release1Year{ 1994 }; + const int release1OriginalYear{ 1993 }; + + ScopedTrack track1A{ session, "MyTrack1A" }; + ScopedTrack track1B{ session, "MyTrack1B" }; + ScopedTrack track2A{ session, "MyTrack2A" }; + ScopedTrack track2B{ session, "MyTrack2B" }; + + { + auto transaction{ session.createReadTransaction() }; + + const auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000))) }; + EXPECT_EQ(releases.results.size(), 0); + } + + { + auto transaction{ session.createWriteTransaction() }; + + track1A.get().modify()->setRelease(release1.get()); + track1B.get().modify()->setRelease(release1.get()); + track2A.get().modify()->setRelease(release2.get()); + track2B.get().modify()->setRelease(release2.get()); + + + track1A.get().modify()->setYear(release1Year); + track1B.get().modify()->setYear(release1Year); + track1A.get().modify()->setOriginalYear(release1OriginalYear); + track1B.get().modify()->setOriginalYear(release1OriginalYear); + + EXPECT_EQ(release1.get()->getYear(), release1Year); + EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalYear); } { diff --git a/src/libs/database/test/Track.cpp b/src/libs/database/test/Track.cpp index 39cd5258..a6b1d91b 100644 --- a/src/libs/database/test/Track.cpp +++ b/src/libs/database/test/Track.cpp @@ -126,7 +126,8 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) TEST_F(DatabaseFixture, Track_date) { ScopedTrack track{ session, "MyTrack" }; - + const Wt::WDate date{ 1995, 5, 5 }; + const Wt::WDate originalDate{ 1994, 2, 2 }; { auto transaction{ session.createReadTransaction() }; EXPECT_EQ(track->getYear(), std::nullopt); @@ -135,14 +136,28 @@ TEST_F(DatabaseFixture, Track_date) { auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 }); - track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 }); + track.get().modify()->setDate(date); + track.get().modify()->setOriginalDate(originalDate); } { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(track->getYear(), 1995); - EXPECT_EQ(track->getOriginalYear(), 1994); + EXPECT_EQ(track->getYear(), std::nullopt); + EXPECT_EQ(track->getOriginalYear(), std::nullopt); + EXPECT_EQ(track->getDate(), date); + EXPECT_EQ(track->getOriginalDate(), originalDate); + } + + { + auto transaction{ session.createWriteTransaction() }; + track.get().modify()->setYear(date.year()); + track.get().modify()->setOriginalYear(originalDate.year()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(track->getYear(), date.year()); + EXPECT_EQ(track->getOriginalYear(), originalDate.year()); } } diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 49ba3000..b821bd66 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -333,26 +333,22 @@ namespace MetaData } else if (tag == "DATE") { - // Higher priority than YEAR if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) track.date = date; + else if (!track.year) + track.year = StringUtils::readAs(value); } - else if (tag == "YEAR" && !track.date.isValid()) - { - // lower priority than DATE - track.date = Utils::parseDate(value); - } + else if (tag == "YEAR") + track.year = StringUtils::readAs(value); else if (tag == "ORIGINALDATE") { - // Higher priority than ORIGINALYEAR if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid()) track.originalDate = date; + else if (!track.originalYear) + track.originalYear = StringUtils::readAs(value); } - else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) - { - // Lower priority than ORIGINALDATE - track.originalDate = Utils::parseDate(value); - } + else if (tag == "ORIGINALYEAR") + track.originalYear = StringUtils::readAs(value); else if (tag == "METADATA_BLOCK_PICTURE") track.hasCover = true; else if (tag == "COPYRIGHT") @@ -510,6 +506,14 @@ namespace MetaData for (const auto& [tag, values] : tags) processTag(track, tag, values, debug); + // If a file has date but no year, set it + if (!track.year && track.date.isValid()) + track.year = track.date.year(); + + // If a file has originalDate but no originalYear, set it + if (!track.originalYear && track.originalDate.isValid()) + track.originalYear = track.originalDate.year(); + return track; } diff --git a/src/libs/metadata/impl/Utils.cpp b/src/libs/metadata/impl/Utils.cpp index fb8f3f02..a0354cbe 100644 --- a/src/libs/metadata/impl/Utils.cpp +++ b/src/libs/metadata/impl/Utils.cpp @@ -37,17 +37,23 @@ namespace MetaData::Utils for (const char* format : formats) { - std::tm tm = {}; + std::tm tm{}; + tm.tm_mon = -1; + tm.tm_mday = -1; + std::istringstream ss{ std::string {dateStr} }; // TODO, remove extra copy here ss >> std::get_time(&tm, format); if (ss.fail()) continue; + if (tm.tm_mday <= 0 || tm.tm_mon < 0) + continue; + const Wt::WDate res { - tm.tm_year + 1900, // years since 1900 - tm.tm_mon + 1, // months since January – [0, 11] - tm.tm_mday ? tm.tm_mday : 1 // day of the month – [1, 31] + tm.tm_year + 1900, // tm.tm_year: years since 1900 + tm.tm_mon + 1, // tm.tm_mon: months since January – [00, 11] + tm.tm_mday // tm.tm_mday: day of the month – [1, 31] }; if (!res.isValid()) continue; diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 2f4399b5..cbd51aa4 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -82,7 +82,9 @@ namespace MetaData Tags userExtraTags; std::chrono::milliseconds duration{}; std::size_t bitrate{}; + std::optional year{}; Wt::WDate date; + std::optional originalYear{}; Wt::WDate originalDate; bool hasCover{}; std::optional acoustID; diff --git a/src/libs/metadata/test/Utils.cpp b/src/libs/metadata/test/Utils.cpp index 41b9ee9e..50b1ff3a 100644 --- a/src/libs/metadata/test/Utils.cpp +++ b/src/libs/metadata/test/Utils.cpp @@ -24,94 +24,98 @@ TEST(MetaData, parseDate) { - using namespace MetaData::Utils; + using namespace MetaData::Utils; - struct TestCase - { - std::string str; - Wt::WDate result; - } testCases [] - { - { "1995-05-09", Wt::WDate {1995, 5, 9} }, - { "1995-01-01", Wt::WDate {1995, 1, 1} }, - { "1900-01-01", Wt::WDate {1900, 1, 1} }, - { "1899-01-01", Wt::WDate {1899, 1, 1} }, - { "1899-12-31", Wt::WDate {1899, 12, 31} }, - { "1899-11-30", Wt::WDate {1899, 11, 30} }, - { "1500-11-30", Wt::WDate {1500, 11, 30} }, - { "1000-11-30", Wt::WDate {1000, 11, 30} }, - { "1899-11-31", Wt::WDate {} }, // invalid day - { "1899-13-01", Wt::WDate {} }, // invalid month - { "1899-11", Wt::WDate {1899, 11, 1} }, // missing day - { "1899", Wt::WDate {1899, 1, 1} }, // missing month and days - { "1600", Wt::WDate {1600, 1, 1} }, // missing month and days - { "1995/05/09", Wt::WDate {1995, 5, 9} }, - { "1995/01/01", Wt::WDate {1995, 1, 1} }, - { "1900/01/01", Wt::WDate {1900, 1, 1} }, - { "1899/01/01", Wt::WDate {1899, 1, 1} }, - { "1899/12/31", Wt::WDate {1899, 12, 31} }, - { "1899/11/30", Wt::WDate {1899, 11, 30} }, - { "1500/11/30", Wt::WDate {1500, 11, 30} }, - { "1000/11/30", Wt::WDate {1000, 11, 30} }, - { "1899/11/31", Wt::WDate {} }, // invalid day - { "1899/13/01", Wt::WDate {} }, // invalid month - { "1899/11", Wt::WDate {1899, 11, 1} }, // missing day - { "1899", Wt::WDate {1899, 1, 1} }, // missing month and days - { "1600", Wt::WDate {1600, 1, 1} }, // missing month and days - { "1995/05-09", Wt::WDate {} }, // invalid mixup separators - { "1995-05/09", Wt::WDate {} }, // invalid mixup separators - }; + struct TestCase + { + std::string str; + Wt::WDate result; + } testCases[] + { + { "1995-05-09", Wt::WDate {1995, 5, 9} }, + { "1995-01-01", Wt::WDate {1995, 1, 1} }, + { "1900-01-01", Wt::WDate {1900, 1, 1} }, + { "1899-01-01", Wt::WDate {1899, 1, 1} }, + { "1899-12-31", Wt::WDate {1899, 12, 31} }, + { "1899-11-30", Wt::WDate {1899, 11, 30} }, + { "1500-11-30", Wt::WDate {1500, 11, 30} }, + { "1000-11-30", Wt::WDate {1000, 11, 30} }, + { "1899-11-31", Wt::WDate {} }, // invalid day + { "1899-11-00", Wt::WDate {} }, // invalid day + { "1899-13-01", Wt::WDate {} }, // invalid month + { "1899-00-01", Wt::WDate {} }, // invalid month + { "1899-11", Wt::WDate {} }, // missing day + { "1899", Wt::WDate {} }, // missing month and days + { "1600", Wt::WDate {} }, // missing month and days + { "1995/05/09", Wt::WDate {1995, 5, 9} }, + { "1995/01/01", Wt::WDate {1995, 1, 1} }, + { "1900/01/01", Wt::WDate {1900, 1, 1} }, + { "1899/01/01", Wt::WDate {1899, 1, 1} }, + { "1899/12/31", Wt::WDate {1899, 12, 31} }, + { "1899/11/30", Wt::WDate {1899, 11, 30} }, + { "1500/11/30", Wt::WDate {1500, 11, 30} }, + { "1000/11/30", Wt::WDate {1000, 11, 30} }, + { "1899/11/31", Wt::WDate {} }, // invalid day + { "1899/11/00", Wt::WDate {} }, // invalid day + { "1899/13/01", Wt::WDate {} }, // invalid month + { "1899/00/01", Wt::WDate {} }, // invalid month + { "1899/11", Wt::WDate {} }, // missing day + { "1899", Wt::WDate {} }, // missing month and days + { "1600", Wt::WDate {} }, // missing month and days + { "1995/05-09", Wt::WDate {} }, // invalid mixup separators + { "1995-05/09", Wt::WDate {} }, // invalid mixup separators + }; - for (const TestCase& testCase : testCases) - { - const Wt::WDate parsed {parseDate(testCase.str)}; + for (const TestCase& testCase : testCases) + { + const Wt::WDate parsed{ parseDate(testCase.str) }; - EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'"; - EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'"; - EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'"; - } + EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'"; + EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'"; + EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'"; + } } TEST(MetaData, extractPerformerAndRole) { - using namespace MetaData::Utils; + using namespace MetaData::Utils; - struct TestCase - { - std::string str; - std::string expectedArtistName; - std::string expectedRole; - } testCases [] - { - { "", "", "" }, - { "(myrole)", "", "myrole" }, - { "(my role)", "", "my role" }, - { " ( my role ) ", "", "my role" }, - { " (()) ", "", "()" }, - { ")", ")", "" }, - { "(", "(", "" }, - { "artist name (my role)", "artist name", "my role" }, - { "artist name ()", "artist name", "" }, - { "artist name ( )", "artist name", "" }, - { "artist (subname) name", "artist (subname) name", "" }, - { " artist name ( my role )", "artist name", "my role" }, - { "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" }, - { "artist name", "artist name", "" }, - { " artist name ", "artist name", "" }, - { "artist name (", "artist name (", "" }, - { "artist name )", "artist name )", "" }, - { "artist name (()", "artist name (", "" }, - { "artist name (())", "artist name", "()" }, - { "artist name ( () )", "artist name", "()" }, - { "artist name (drums (drum set))", "artist name", "drums (drum set)" }, - { "artist name ( drums (drum set) )", "artist name", "drums (drum set)" }, - }; + struct TestCase + { + std::string str; + std::string expectedArtistName; + std::string expectedRole; + } testCases[] + { + { "", "", "" }, + { "(myrole)", "", "myrole" }, + { "(my role)", "", "my role" }, + { " ( my role ) ", "", "my role" }, + { " (()) ", "", "()" }, + { ")", ")", "" }, + { "(", "(", "" }, + { "artist name (my role)", "artist name", "my role" }, + { "artist name ()", "artist name", "" }, + { "artist name ( )", "artist name", "" }, + { "artist (subname) name", "artist (subname) name", "" }, + { " artist name ( my role )", "artist name", "my role" }, + { "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" }, + { "artist name", "artist name", "" }, + { " artist name ", "artist name", "" }, + { "artist name (", "artist name (", "" }, + { "artist name )", "artist name )", "" }, + { "artist name (()", "artist name (", "" }, + { "artist name (())", "artist name", "()" }, + { "artist name ( () )", "artist name", "()" }, + { "artist name (drums (drum set))", "artist name", "drums (drum set)" }, + { "artist name ( drums (drum set) )", "artist name", "drums (drum set)" }, + }; - for (const TestCase& testCase : testCases) - { - PerformerArtist performer {extractPerformerAndRole(testCase.str)}; + for (const TestCase& testCase : testCases) + { + PerformerArtist performer{ extractPerformerAndRole(testCase.str) }; - EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'"; - EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; - } + EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'"; + EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'"; + } } diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index ab638865..3b0abc0f 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -443,11 +443,17 @@ namespace Scanner track.modify()->setTrackNumber(trackInfo->position); track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt); track.modify()->setDate(trackInfo->date); + track.modify()->setYear(trackInfo->year); track.modify()->setOriginalDate(trackInfo->originalDate); + track.modify()->setOriginalYear(trackInfo->originalYear); - // If a file has an OriginalYear but no Year, set it to ease filtering + // If a file has an OriginalDate but no date, set it to ease filtering if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) track.modify()->setDate(trackInfo->originalDate); + + // If a file has an OriginalYear but no Year, set it to ease filtering + if (!trackInfo->year && trackInfo->originalYear) + track.modify()->setYear(trackInfo->originalYear); track.modify()->setRecordingMBID(trackInfo->recordingMBID); track.modify()->setTrackMBID(trackInfo->mbid); diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 2ba6eda8..07394e94 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(lmssubsonic SHARED impl/responses/Bookmark.cpp impl/responses/Contributor.cpp impl/responses/DiscTitle.cpp + impl/responses/ItemDate.cpp impl/responses/ItemGenre.cpp impl/responses/Genre.cpp impl/responses/Playlist.cpp diff --git a/src/libs/subsonic/impl/ProtocolVersion.hpp b/src/libs/subsonic/impl/ProtocolVersion.hpp index ba2c5788..c527592d 100644 --- a/src/libs/subsonic/impl/ProtocolVersion.hpp +++ b/src/libs/subsonic/impl/ProtocolVersion.hpp @@ -31,7 +31,7 @@ namespace API::Subsonic }; static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 }; - static inline constexpr std::string_view serverVersion{ "5" }; + static inline constexpr std::string_view serverVersion{ "6" }; } namespace StringUtils diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 412b5bd7..f570c959 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -30,6 +30,7 @@ #include "responses/Artist.hpp" #include "responses/DiscTitle.hpp" +#include "responses/ItemDate.hpp" #include "responses/ItemGenre.hpp" #include "SubsonicId.hpp" @@ -58,8 +59,8 @@ namespace API::Subsonic albumNode.setAttribute("created", StringUtils::toISO8601String(release->getLastWritten())); albumNode.setAttribute("id", idToString(release->getId())); albumNode.setAttribute("coverArt", idToString(release->getId())); - if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid()) - albumNode.setAttribute("year", releaseDate.year()); + if (const auto year{ release->getYear() }) + albumNode.setAttribute("year", *year); auto artists{ release->getReleaseArtists() }; if (artists.empty()) @@ -149,11 +150,7 @@ namespace API::Subsonic albumNode.addArrayChild("artists", createArtistNode(artist)); albumNode.setAttribute("displayArtist", release->getArtistDisplayName()); - - { - const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() }; - albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : ""); - } + albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate(), release->getOriginalYear())); { bool isCompilation{}; diff --git a/src/libs/subsonic/impl/responses/ItemDate.cpp b/src/libs/subsonic/impl/responses/ItemDate.cpp new file mode 100644 index 00000000..b8f41e7b --- /dev/null +++ b/src/libs/subsonic/impl/responses/ItemDate.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "responses/Genre.hpp" + +#include "database/Cluster.hpp" + +namespace API::Subsonic +{ + Response::Node createItemDateNode(const Wt::WDate& date, std::optional year) + { + Response::Node itemDateNode; + + if (date.isValid()) + { + itemDateNode.setAttribute("year", date.year()); + itemDateNode.setAttribute("month", date.month()); + itemDateNode.setAttribute("day", date.day()); + } + else if (year) + { + itemDateNode.setAttribute("year", *year); + } + + return itemDateNode; + } +} \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/ItemDate.hpp b/src/libs/subsonic/impl/responses/ItemDate.hpp new file mode 100644 index 00000000..e9af4a33 --- /dev/null +++ b/src/libs/subsonic/impl/responses/ItemDate.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include "SubsonicResponse.hpp" + +namespace API::Subsonic +{ + Response::Node createItemDateNode(const Wt::WDate& date, std::optional year); +} diff --git a/src/libs/utils/test/String.cpp b/src/libs/utils/test/String.cpp index 6b8dc78d..fff9c076 100644 --- a/src/libs/utils/test/String.cpp +++ b/src/libs/utils/test/String.cpp @@ -155,7 +155,7 @@ TEST(StringUtils, escapeString) EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|"); } -TEST(StringUtils, readAs) +TEST(StringUtils, readAs_bool) { EXPECT_EQ(StringUtils::readAs("true"), true); EXPECT_EQ(StringUtils::readAs("1"), true); @@ -165,6 +165,19 @@ TEST(StringUtils, readAs) EXPECT_EQ(StringUtils::readAs(""), std::nullopt); } +TEST(StringUtils, readAs_int) +{ + EXPECT_EQ(StringUtils::readAs("1024"), 1024); + EXPECT_EQ(StringUtils::readAs("0"), 0); + EXPECT_EQ(StringUtils::readAs("-0"), 0); + EXPECT_EQ(StringUtils::readAs("-1"), -1); + EXPECT_EQ(StringUtils::readAs(""), std::nullopt); + EXPECT_EQ(StringUtils::readAs("a"), std::nullopt); + EXPECT_EQ(StringUtils::readAs("1024-1"), 1024); + EXPECT_EQ(StringUtils::readAs("1024-"), 1024); + EXPECT_EQ(StringUtils::readAs("1024/5"), 1024); +} + TEST(StringUtils, capitalize) { struct TestCase diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 2663ecf4..38171378 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -57,7 +57,7 @@ namespace UserInterface::ReleaseListHelpers if (showYear) { - Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) }; + Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) }; if (!year.empty()) { entry->setCondition("if-has-year", true); @@ -136,18 +136,18 @@ namespace UserInterface::ReleaseHelpers return res; } - Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate) + Wt::WString buildReleaseYearString(std::optional year, std::optional originalYear) { Wt::WString res; // Year can be here, but originalYear can't be here without year (enforced by scanner) - if (!releaseDate.isValid()) + if (!year) return res; - if (originalReleaseDate.isValid() && originalReleaseDate.year() != releaseDate.year()) - res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")"; + if (originalYear && *originalYear != *year) + res = std::to_string(*originalYear) + " (" + std::to_string(*year) + ")"; else - res = std::to_string(releaseDate.year()); + res = std::to_string(*year); return res; } diff --git a/src/lms/ui/explore/ReleaseHelpers.hpp b/src/lms/ui/explore/ReleaseHelpers.hpp index aab70f29..12f9d8e8 100644 --- a/src/lms/ui/explore/ReleaseHelpers.hpp +++ b/src/lms/ui/explore/ReleaseHelpers.hpp @@ -45,5 +45,5 @@ namespace UserInterface::ReleaseListHelpers namespace UserInterface::ReleaseHelpers { Wt::WString buildReleaseTypeString(const ReleaseType& releaseType); - Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate); + Wt::WString buildReleaseYearString(std::optional year, std::optional originalYear); } diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index c8f01c25..a4f8c30a 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -247,7 +247,7 @@ namespace UserInterface bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); - Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) }; + Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) }; if (!year.empty()) { setCondition("if-has-year", true); diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index ff9bb7b1..ee52facc 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -100,8 +100,8 @@ namespace UserInterface { std::string releaseName; - if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid()) - releaseName += std::to_string(releaseDate.year()) + " - "; + if (const auto year{ release->getYear() }) + releaseName += std::to_string(*year) + " - "; releaseName += StringUtils::replaceInString(release->getName(), "/", "_"); return releaseName; diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 06139132..c5820764 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -172,9 +172,13 @@ namespace if (track->date.isValid()) std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl; + if (track->year) + std::cout << "Year: " << *track->year << std::endl; if (track->originalDate.isValid()) std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl; + if (track->originalYear) + std::cout << "Original year: " << *track->originalYear << std::endl; std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl; From ead3768b411829da0d51ca6c81cd0d9f8f3abc3a Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 10 Jan 2024 23:46:20 +0100 Subject: [PATCH 2/4] ListenBrainz: added track duration in addition_info field. fixes #403 --- .../scrobbling/impl/listenbrainz/ListensSynchronizer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index db776aa9..a964686d 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -64,6 +64,7 @@ namespace Wt::Json::Object additionalInfo; additionalInfo["listening_from"] = "LMS"; + additionalInfo["duration_ms"] = std::chrono::duration_cast(track->getDuration()).count(); if (track->getRelease()) { if (auto MBID{ track->getRelease()->getMBID() }) From 1f9387b703c62db8d4e748458a2a9c4064308df4 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 12 Jan 2024 23:19:49 +0100 Subject: [PATCH 3/4] Search first artist files whose name is artist's mbid then artist's name. fixes #406 --- conf/lms.conf | 1 + src/libs/services/cover/impl/CoverService.cpp | 77 +++++++++++++++---- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/conf/lms.conf b/conf/lms.conf index 34583eee..ef239776 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -87,6 +87,7 @@ cover-jpeg-quality = 75; cover-preferred-file-names = ("cover", "front"); # File names for artist images (order is important) +# Files whose name is the artist's MBID, then the artist's name, are searched before the names in this list artist-image-file-names = ("artist"); # Playqueue max entry count diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 516267ca..1a941a29 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -24,6 +24,7 @@ #include "av/IAudioFile.hpp" #include "database/Db.hpp" +#include "database/Artist.hpp" #include "database/Release.hpp" #include "database/Session.hpp" #include "database/Track.hpp" @@ -426,36 +427,86 @@ namespace Cover if (artistImage) return artistImage; - std::set parentPaths; + std::string artistName; + std::string artistMBID; + + std::set releasePaths; + std::set multiArtistReleasePaths; + { Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + const Artist::pointer artist{ Artist::find(session, artistId) }; + if (!artist) + return artistImage; + + artistName = artist->getName(); + if (auto mbid{ artist->getMBID() }) + artistMBID = mbid->getAsString(); + Track::FindParameters params; params.setArtist(artistId, { TrackArtistLinkType::ReleaseArtist }); - auto transaction{ session.createReadTransaction() }; - Track::find(session, params, [&](const Track::pointer& track) { - parentPaths.insert(track->getPath().parent_path()); + Artist::FindParameters artistFindParams; + artistFindParams.setTrack(track->getId()); + artistFindParams.setLinkType(TrackArtistLinkType::ReleaseArtist); + + const auto releaseArtists{ Artist::findIds(session, artistFindParams) }; + if (releaseArtists.results.size() == 1) + releasePaths.insert(track->getPath().parent_path()); + else + multiArtistReleasePaths.insert(track->getPath().parent_path()); }); } - if (parentPaths.size() == 1) + std::vector artistFileNames; + if (!artistMBID.empty()) + artistFileNames.push_back(artistMBID); + artistFileNames.push_back(artistName); + + std::vector artistFileNamesWithGenericNames{ artistFileNames }; + artistFileNamesWithGenericNames.insert(artistFileNamesWithGenericNames.end(), std::cbegin(_artistFileNames), std::cend(_artistFileNames)); + + // Expect layout like this: + // ReleaseArtist/Release/Tracks' + // /artist-mbid.jpg + // /artist-name.jpg + // /artist.jpg + if (!releasePaths.empty()) { - artistImage = getFromDirectory(parentPaths.begin()->parent_path(), width, _artistFileNames, false); - } - else if (parentPaths.size() > 1) - { - const std::filesystem::path longestCommonPath{ PathUtils::getLongestCommonPath(std::cbegin(parentPaths), std::cend(parentPaths)) }; - artistImage = getFromDirectory(longestCommonPath, width, _artistFileNames, false); + const std::filesystem::path artistPath{ releasePaths.size() == 1 ? releasePaths.begin()->parent_path() : PathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) }; + artistImage = getFromDirectory(artistPath, width, artistFileNamesWithGenericNames, false); } + // Expect layout like this: + // ReleaseArtist/Release/Tracks' + // /artist-mbid.jpg + // /artist-name.jpg + // /artist.jpg if (!artistImage) { - for (const std::filesystem::path& parentPath : parentPaths) + for (const std::filesystem::path& releasePath : releasePaths) { - artistImage = getFromDirectory(parentPath, width, _artistFileNames, false); + artistImage = getFromDirectory(releasePath, width, artistFileNamesWithGenericNames, false); + if (artistImage) + break; + } + } + + // Expect layout like this: + // Only search for the artist's name in the release path, as we can't map a generic name to several artists + // ReleaseArtist/Release/Tracks' + // /artist-name.jpg + // /artist-mbid.jpg + if (!artistImage) + { + for (const std::filesystem::path& releasePath : multiArtistReleasePaths) + { + artistImage = getFromDirectory(releasePath, width, artistFileNames, false); if (artistImage) break; } From f5f577af521ee5fe23b31dc9343fca7689c3b47a Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 12 Jan 2024 23:20:22 +0100 Subject: [PATCH 4/4] Added TODO --- src/lms/ui/explore/ReleaseView.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index a4f8c30a..a4c64593 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -394,6 +394,7 @@ namespace UserInterface entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); const auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) }; + // TODO: display artist if it is single and not the one of the release (variousArtists is false in that case) if (variousArtists && !artists.empty()) { entry->setCondition("if-has-artists", true);