diff --git a/approot/artist.xml b/approot/artist.xml index 2b3f5730..24045219 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -4,7 +4,7 @@
-

${name}

+

${name}

${clusters class="mb-2"}
${play-btn class="btn btn-primary"} @@ -20,17 +20,21 @@
- ${} + ${} + ${}

${tr:Lms.Explore.releases}

${
} ${releases class="mb-3"} - ${
} - ${} + ${} + ${} + ${}

${tr:Lms.Explore.Artist.appears-on}

${
} + ${appears-on-releases class="mb-3"} + ${
} + ${} + ${}

${tr:Lms.Explore.tracks}

${
} ${tracks class="mb-3"} - ${
} + ${} ${} -
-

${tr:Lms.Explore.Artist.similar-artists}

- ${similar-artists class="d-grid gap-3"} -
+

${tr:Lms.Explore.Artist.similar-artists}

+ ${similar-artists class="d-grid gap-3"} ${
}
diff --git a/approot/messages.xml b/approot/messages.xml index 0acf1da1..93d68433 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -137,6 +137,7 @@ Various artists +Appears on Similar artists diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index b4572718..7a9c708a 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -137,6 +137,7 @@ Artistes divers +Apparitions Artistes similaires diff --git a/approot/messages_it.xml b/approot/messages_it.xml index b8648405..7fd6fdfd 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -136,6 +136,7 @@ Vari artisti + Artisti simili diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index fca04e46..094fffa0 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -137,6 +137,7 @@ 群星 + 相似歌手 diff --git a/approot/release.xml b/approot/release.xml index d964b8c2..715c2d3d 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -12,7 +12,7 @@ ${cover}
-

${name}

+

${name}

${}${artists}${} ${}
${tr:Lms.Explore.various-artists}
${
}
${}${year}${} (${orig-year})${} · ${}${duration}
diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index c268ef7c..4513f9cd 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -41,7 +41,8 @@ createQuery(Session& session, const Release::FindParameters& params) if (params.sortMethod == ReleaseSortMethod::LastWritten || params.writtenAfter.isValid() - || params.dateRange) + || params.dateRange + || params.artist.isValid()) { query.join("track t ON t.release_id = r.id"); } @@ -66,6 +67,55 @@ createQuery(Session& session, const Release::FindParameters& params) .where("s_r.scrobbler = ?").bind(*params.scrobbler); } + if (params.artist.isValid()) + { + query.join("artist a ON a.id = t_a_l.artist_id") + .join("track_artist_link t_a_l ON t_a_l.track_id = t.id") + .where("a.id = ?").bind(params.artist); + + if (!params.trackArtistLinkTypes.empty()) + { + std::ostringstream oss; + + bool first {true}; + for (TrackArtistLinkType linkType : params.trackArtistLinkTypes) + { + if (!first) + oss << " OR "; + oss << "t_a_l.type = ?"; + query.bind(linkType); + + first = false; + } + query.where(oss.str()); + } + + if (!params.excludedTrackArtistLinkTypes.empty()) + { + std::ostringstream oss; + oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" + " INNER JOIN artist a ON a.id = t_a_l.artist_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" + " INNER JOIN track t ON t.release_id = r.id" + " WHERE (a.id = ? AND ("; + + query.bind(params.artist); + + bool first {true}; + for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes) + { + if (!first) + oss << " OR "; + oss << "t_a_l.type = ?"; + query.bind(linkType); + + first = false; + } + oss << ")))"; + query.where(oss.str()); + } + } + if (!params.clusters.empty()) { std::ostringstream oss; @@ -103,6 +153,9 @@ createQuery(Session& session, const Release::FindParameters& params) case ReleaseSortMethod::Date: query.orderBy("t.date, r.name COLLATE NOCASE"); break; + case ReleaseSortMethod::DateDesc: + query.orderBy("t.date DESC, r.name COLLATE NOCASE"); + break; case ReleaseSortMethod::StarredDateDesc: assert(params.starringUser.isValid()); query.orderBy("s_r.date_time DESC"); diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp index 660e7082..ec41e43c 100644 --- a/src/libs/services/database/impl/Session.cpp +++ b/src/libs/services/database/impl/Session.cpp @@ -151,9 +151,9 @@ Session::prepareTables() _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)"); diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 999899a6..ed11e694 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -25,11 +25,13 @@ #include #include +#include "services/database/ArtistId.hpp" #include "services/database/ClusterId.hpp" #include "services/database/Object.hpp" #include "services/database/ReleaseId.hpp" #include "services/database/Types.hpp" #include "services/database/UserId.hpp" +#include "utils/EnumSet.hpp" #include "utils/UUID.hpp" namespace Database @@ -54,8 +56,11 @@ class Release : public Object Range range; Wt::WDateTime writtenAfter; std::optional dateRange; - UserId starringUser; // only releases starred by this user - std::optional scrobbler; // and for this scrobbler + UserId starringUser; // only releases starred by this user + std::optional scrobbler; // and for this scrobbler + ArtistId artist; // only releases that involved this user + EnumSet trackArtistLinkTypes; // and for these link types + EnumSet excludedTrackArtistLinkTypes; // but not for these link types FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } @@ -64,6 +69,13 @@ class Release : public Object FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; } FindParameters& setDateRange(const std::optional& _dateRange) {dateRange = _dateRange; return *this; } FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}, EnumSet _excludedTrackArtistLinkTypes = {}) + { + artist = _artist; + trackArtistLinkTypes = _trackArtistLinkTypes; + excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes; + return *this; + } }; Release() = default; diff --git a/src/libs/services/database/include/services/database/TrackArtistLink.hpp b/src/libs/services/database/include/services/database/TrackArtistLink.hpp index 8a1ba0b2..5d565da5 100644 --- a/src/libs/services/database/include/services/database/TrackArtistLink.hpp +++ b/src/libs/services/database/include/services/database/TrackArtistLink.hpp @@ -54,7 +54,6 @@ namespace Database void persist(Action& a) { Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _type, "name"); Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); @@ -62,7 +61,6 @@ namespace Database private: TrackArtistLinkType _type; - std::string _name; Wt::Dbo::ptr _track; Wt::Dbo::ptr _artist; diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index 4333be45..0467fe80 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -98,6 +98,7 @@ namespace Database None, Name, Date, + DateDesc, Random, LastWritten, StarredDateDesc, diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index 9ff1db24..bd18a13f 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -396,4 +396,64 @@ TEST_F(DatabaseFixture, Release_writtenAfter) } } +TEST_F(DatabaseFixture, Release_artist) +{ + ScopedRelease release {session, "MyRelease"}; + ScopedTrack track {session, "MyTrack"}; + ScopedArtist artist {session, "MyArtist"}; + ScopedArtist artist2 {session, "MyArtist2"}; + { + auto transaction {session.createUniqueTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))}; + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist})); + EXPECT_EQ(releases.results.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + + auto releases {Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer})); + EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId(), {TrackArtistLinkType::Artist})); + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist2.getId())); + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist})); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId())); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer})); + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer})); + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist})); + EXPECT_EQ(releases.results.size(), 0); + + releases = Release::find(session, Release::FindParameters {}.setArtist(artist.getId(), {}, {TrackArtistLinkType::Artist, TrackArtistLinkType::Composer})); + EXPECT_EQ(releases.results.size(), 0); + } +} diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 4d6212ab..5fad227b 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -111,11 +111,25 @@ Artist::refreshView() _artistId = *artistId; - refreshReleases(artist); - refreshNonReleaseTracks(artist); + std::size_t sectionCount{}; + + LMS_LOG(UI, DEBUG) << "Refresh releases..."; + if (refreshReleases()) + sectionCount++; + LMS_LOG(UI, DEBUG) << "Refresh appears on releases..."; + if (refreshAppearsOnReleases()) + sectionCount++; + LMS_LOG(UI, DEBUG) << "Refresh non album tracks..."; + if (refreshNonReleaseTracks(artist)) + sectionCount++; + + LMS_LOG(UI, DEBUG) << "Refresh links..."; refreshLinks(artist); refreshSimilarArtists(similarArtistIds); + if (sectionCount > 1) + setCondition("if-section-titles", true); + Wt::WContainerWidget* clusterContainers {bindNew("clusters")}; { @@ -177,35 +191,63 @@ Artist::refreshView() } } -void -Artist::refreshReleases(const ObjectPtr& artist) +bool +Artist::refreshReleases() { - if (artist->getReleaseCount() == 0) - return; - - setCondition("if-has-release", true); _releaseContainer = bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); _releaseContainer->onRequestElements.connect(this, [this] { - addSomeReleases(); + addSomeReleases(*_releaseContainer, {TrackArtistLinkType::ReleaseArtist}, {}); }); - addSomeReleases(); + + const bool added {addSomeReleases(*_releaseContainer, {TrackArtistLinkType::ReleaseArtist}, {})}; + setCondition("if-has-releases", added); + + return added; } -void +bool +Artist::refreshAppearsOnReleases() +{ + const EnumSet types + { + TrackArtistLinkType::Artist, + TrackArtistLinkType::Arranger, + TrackArtistLinkType::Composer, + TrackArtistLinkType::Conductor, + TrackArtistLinkType::Lyricist, + TrackArtistLinkType::Mixer, + TrackArtistLinkType::Performer, + TrackArtistLinkType::Producer, + TrackArtistLinkType::Remixer, + TrackArtistLinkType::Writer, + }; + + _appearsOnReleaseContainer = bindNew("appears-on-releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); + _appearsOnReleaseContainer->onRequestElements.connect(this, [=] + { + addSomeReleases(*_appearsOnReleaseContainer, types, {TrackArtistLinkType::ReleaseArtist}); + }); + + const bool added {addSomeReleases(*_appearsOnReleaseContainer, types, {TrackArtistLinkType::ReleaseArtist})}; + setCondition("if-has-appears-on-releases", added); + return added; +} + +bool Artist::refreshNonReleaseTracks(const ObjectPtr& artist) { if (!artist->hasNonReleaseTracks()) - return; + return false; - setCondition("if-has-non-release-track", true); + setCondition("if-has-non-release-tracks", true); _trackContainer = bindNew("tracks"); _trackContainer->onRequestElements.connect(this, [this] { addSomeNonReleaseTracks(); }); - addSomeNonReleaseTracks(); + return addSomeNonReleaseTracks(); } void @@ -238,35 +280,46 @@ Artist::refreshLinks(const Database::Artist::pointer& artist) } } -void -Artist::addSomeReleases() +bool +Artist::addSomeReleases(InfiniteScrollingContainer& releaseContainer, EnumSet linkTypes, EnumSet excludedLinkTypes) { + bool areArtistsAdded{}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; if (!artist) - return; + return areArtistsAdded; - const Range range {static_cast(_releaseContainer->getCount()), _releasesBatchSize}; - const auto releases {artist->getReleases(range, _filters->getClusterIds())}; + const Range range {static_cast(releaseContainer.getCount()), _releasesBatchSize}; + Release::FindParameters params; + params.setClusters(_filters->getClusterIds()); + params.setArtist(_artistId, linkTypes, excludedLinkTypes); + params.setRange(range); + params.setSortMethod(ReleaseSortMethod::DateDesc); + + const auto releases {Release::find(LmsApp->getDbSession(), params)}; for (const ReleaseId releaseId : releases.results) { const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)}; - _releaseContainer->add(ReleaseListHelpers::createEntryForArtist(release, artist)); + releaseContainer.add(ReleaseListHelpers::createEntryForArtist(release, artist)); + areArtistsAdded = true; } - _releaseContainer->setHasMore(releases.moreResults); + releaseContainer.setHasMore(releases.moreResults); + + return areArtistsAdded; } -void +bool Artist::addSomeNonReleaseTracks() { + bool areTracksAdded{}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; if (!artist) - return; + return areTracksAdded; const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {static_cast(_trackContainer->getCount()), _tracksBatchSize})}; bool moreResults {tracks.moreResults}; @@ -280,9 +333,13 @@ Artist::addSomeNonReleaseTracks() } _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); + + areTracksAdded = true; } _trackContainer->setHasMore(moreResults); + + return areTracksAdded; } } // namespace UserInterface diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 9b7dabb5..ccec3d5d 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -20,6 +20,8 @@ #pragma once #include "services/database/Object.hpp" +#include "services/database/Types.hpp" +#include "utils/EnumSet.hpp" #include "common/Template.hpp" #include "PlayQueueAction.hpp" @@ -46,19 +48,21 @@ namespace UserInterface private: void refreshView(); - void refreshReleases(const Database::ObjectPtr& artist); - void refreshNonReleaseTracks(const Database::ObjectPtr& artist); + bool refreshReleases(); + bool refreshAppearsOnReleases(); + bool refreshNonReleaseTracks(const Database::ObjectPtr& artist); void refreshSimilarArtists(const std::vector& similarArtistsId); void refreshLinks(const Database::ObjectPtr& artist); - void addSomeReleases(); - void addSomeNonReleaseTracks(); + bool addSomeReleases(InfiniteScrollingContainer& container, EnumSet linkTypes, EnumSet excludedLinkTypes); + bool addSomeNonReleaseTracks(); static constexpr std::size_t _releasesBatchSize {6}; static constexpr std::size_t _tracksBatchSize {6}; static constexpr std::size_t _tracksMaxCount {160}; Filters* _filters {}; InfiniteScrollingContainer* _releaseContainer {}; + InfiniteScrollingContainer* _appearsOnReleaseContainer {}; InfiniteScrollingContainer* _trackContainer {}; Database::ArtistId _artistId {}; };