From f70692b680e38a5f486b0282b8c53976fb77e394 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 24 Dec 2025 08:59:56 +0100 Subject: [PATCH] Added a dedicated table for release artists + handle display names in UI/Subsonic API, ref #731 --- approot/artists.xml | 26 +- approot/explore.xml | 3 +- approot/mediaplayer.xml | 4 +- approot/messages.xml | 26 +- approot/messages_es.xml | 26 +- approot/messages_fr.xml | 26 +- approot/messages_it.xml | 26 +- approot/messages_pl.xml | 28 +- approot/messages_zh.xml | 21 +- src/libs/database/CMakeLists.txt | 7 +- src/libs/database/impl/Migration.cpp | 28 +- src/libs/database/impl/Session.cpp | 2 + src/libs/database/impl/objects/Artist.cpp | 23 +- src/libs/database/impl/objects/Directory.cpp | 19 +- src/libs/database/impl/objects/Listen.cpp | 10 +- src/libs/database/impl/objects/Release.cpp | 115 ++++-- .../impl/objects/ReleaseArtistLink.cpp | 157 ++++++++ src/libs/database/impl/objects/Track.cpp | 21 ++ .../database/impl/objects/TrackArtistLink.cpp | 19 +- src/libs/database/impl/objects/Types.cpp | 2 - .../include/database/objects/Artist.hpp | 20 +- .../include/database/objects/Directory.hpp | 15 +- .../include/database/objects/Listen.hpp | 12 +- .../include/database/objects/Release.hpp | 43 ++- .../database/objects/ReleaseArtistLink.hpp | 127 +++++++ .../include/database/objects/Track.hpp | 6 +- .../database/objects/TrackArtistLink.hpp | 9 +- .../include/database/objects/Types.hpp | 8 +- src/libs/database/test/Artist.cpp | 205 ++++++----- src/libs/database/test/CMakeLists.txt | 1 + src/libs/database/test/Cluster.cpp | 30 +- src/libs/database/test/DatabaseTest.cpp | 10 +- src/libs/database/test/Listen.cpp | 88 ++++- src/libs/database/test/Migration.cpp | 2 + src/libs/database/test/Release.cpp | 161 ++++++--- src/libs/database/test/ReleaseArtistLink.cpp | 67 ++++ .../feedback/impl/FeedbackService.cpp | 2 +- .../services/feedback/IFeedbackService.hpp | 12 +- .../audiofile/AudioFileScanOperation.cpp | 99 +++-- .../steps/ScanStepArtistReconciliation.cpp | 50 ++- .../steps/ScanStepAssociateArtistImages.cpp | 4 +- .../scrobbling/impl/ScrobblingService.cpp | 3 +- .../impl/listenbrainz/ListensSynchronizer.cpp | 33 +- .../scrobbling/IScrobblingService.hpp | 8 +- src/libs/subsonic/impl/endpoints/Browsing.cpp | 30 +- src/libs/subsonic/impl/responses/Album.cpp | 46 ++- src/libs/subsonic/impl/responses/Artist.cpp | 40 +- src/libs/subsonic/impl/responses/Artist.hpp | 6 +- .../subsonic/impl/responses/Contributor.cpp | 4 +- .../subsonic/impl/responses/Contributor.hpp | 2 +- src/libs/subsonic/impl/responses/Song.cpp | 20 +- src/lms/CMakeLists.txt | 1 + src/lms/ui/MediaPlayer.cpp | 29 +- src/lms/ui/MediaPlayer.hpp | 3 +- src/lms/ui/PlayQueue.cpp | 4 +- src/lms/ui/SettingsView.cpp | 14 +- src/lms/ui/State.hpp | 2 +- src/lms/ui/Utils.cpp | 342 ++++++++++++------ src/lms/ui/Utils.hpp | 40 +- src/lms/ui/explore/ArtistCollector.cpp | 47 ++- src/lms/ui/explore/ArtistCollector.hpp | 6 +- src/lms/ui/explore/ArtistType.cpp | 56 +++ src/lms/ui/explore/ArtistType.hpp | 52 +++ src/lms/ui/explore/ArtistTypeSelector.hpp | 45 +++ src/lms/ui/explore/ArtistView.cpp | 69 ++-- src/lms/ui/explore/ArtistView.hpp | 6 +- src/lms/ui/explore/ArtistsView.cpp | 20 +- src/lms/ui/explore/ArtistsView.hpp | 8 +- src/lms/ui/explore/ReleaseHelpers.cpp | 105 ++---- src/lms/ui/explore/ReleaseHelpers.hpp | 11 +- src/lms/ui/explore/ReleaseView.cpp | 89 ++--- src/lms/ui/explore/ReleasesView.cpp | 2 +- .../explore/TrackArtistLinkTypeSelector.hpp | 48 --- src/lms/ui/explore/TrackListHelpers.cpp | 70 +--- src/lms/ui/explore/TrackListHelpers.hpp | 11 - src/lms/ui/resource/DownloadResource.cpp | 12 +- src/tools/db-generator/LmsDbGenerator.cpp | 5 +- .../recommendation/LmsRecommendation.cpp | 2 +- 78 files changed, 1916 insertions(+), 935 deletions(-) create mode 100644 src/libs/database/impl/objects/ReleaseArtistLink.cpp create mode 100644 src/libs/database/include/database/objects/ReleaseArtistLink.hpp create mode 100644 src/libs/database/test/ReleaseArtistLink.cpp create mode 100644 src/lms/ui/explore/ArtistType.cpp create mode 100644 src/lms/ui/explore/ArtistType.hpp create mode 100644 src/lms/ui/explore/ArtistTypeSelector.hpp delete mode 100644 src/lms/ui/explore/TrackArtistLinkTypeSelector.hpp diff --git a/approot/artists.xml b/approot/artists.xml index 335cabaf..83f00c4d 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -4,7 +4,7 @@
- ${link-type class="me-1"} + ${artist-type class="me-1"}
${search class="form-control form-control-sm me-1" type="search"} ${sort-mode} @@ -29,22 +29,24 @@ ${loading-indicator} - + diff --git a/approot/explore.xml b/approot/explore.xml index eba82ec2..bd8a7728 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -58,13 +58,14 @@ ${selected-item}
diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml index 168f8499..21e1f759 100644 --- a/approot/mediaplayer.xml +++ b/approot/mediaplayer.xml @@ -27,11 +27,13 @@
${title class="d-block text-truncate"} - ${artist class="d-inline text-decoration-none text-truncate link-success"} + + ${artist class="d-inline text-decoration-none text-truncate"} ${separator} ${release class="text-decoration-none text-truncate link-success"} +
diff --git a/approot/messages.xml b/approot/messages.xml index d7cec9cb..ca602687 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -240,43 +240,43 @@ Similar artists -All artists - - Track artist - Track artists - - +All artists + Composer Composers - + Conductor Conductors - + Lyricist Lyricists - + Mixer Mixers - + Performer Performers - + Producer Producers - + Album artist Album artists - + Remixer Remixers + + Track artist + Track artists + Copyright diff --git a/approot/messages_es.xml b/approot/messages_es.xml index 10d019c8..988e6d78 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -240,43 +240,43 @@ Artistas similares -Todos los artistas - - Artista de la pista - Artistas de la pista - - +Todos los artistas + Compositor Compositores - + Director de orquesta Directores de orquesta - + Letrista Letristas - + Mezclador Mezcladores - + Intérprete Intérpretes - + Productor Productores - + Artista del álbum Artistas del álbum - + Remezclador Remezcladores + + Artista de la pista + Artistas de la pista + Copyright diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 0a241be9..b5229885 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -240,43 +240,43 @@ Artistes similaires -Tous les artistes - - Artiste de piste - Artistes de piste - - +Tous les artistes + Compositeur Compositeurs - + Chef d'orchestre Chefs d'orchestre - + Parolier Paroliers - + Mixeur Mixeurs - + Interprète Interprètes - + Producteur Producteurs - + Artiste d'album Artistes d'album - + Remixeur Remixeurs + + Artiste de piste + Artistes de piste + Copyright diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 56394e18..798dc86e 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -240,43 +240,43 @@ Artisti simili -Tutti gli artisti - - Artista della traccia - Artisti della traccia - - +Tutti gli artisti + Compositore Compositori - + Direttore d'orchestra Direttori d'orchestra - + Paroliere Parolieri - + Tecnico del mixaggio Tecnici del mixaggio - + Interprete Interpreti - + Produttore Produttori - + Artista dell'album Artisti dell'album - + Remixer Remixers + + Artista della traccia + Artisti della traccia + Copyright diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 65328474..84cd5729 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -257,52 +257,52 @@ Podobni artyści -Wszyscy artyści - - Artysta ze ścieżki - Artyści ze ścieżki - Artyści ze ścieżki - - +Wszyscy artyści + Kompozytor Kompozytorzy Kompozytorzy - + Dyrygent Dyrygenci Dyrygenci - + Tekściarz Tekściarze Tekściarze - + Realizator Realizatorzy Realizatorzy - + Wykonawca Wykonawcy Wykonawcy - + Producent Producenci Producenci - + Artysta z albumu Artyści z albumu Artyści z albumu - + Remixer Remikserzy Remikserzy + + Artysta ze ścieżki + Artyści ze ścieżki + Artyści ze ścieżki + Prawa autorskie diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 8f80be0f..8293c6bf 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -238,17 +238,16 @@ 相似歌手 -所有艺术家 -曲目艺术家 -作曲者 -指挥 -作词者 -混音师 -表演者 -制作人 -专辑艺术家 -混音师 - +所有艺术家 +作曲者 +指挥 +作词者 +混音师 +表演者 +制作人 +专辑艺术家 +混音师 +曲目艺术家 版权所有 diff --git a/src/libs/database/CMakeLists.txt b/src/libs/database/CMakeLists.txt index 2ba86032..942a6c29 100644 --- a/src/libs/database/CMakeLists.txt +++ b/src/libs/database/CMakeLists.txt @@ -14,9 +14,7 @@ add_library(lmsdatabase STATIC impl/objects/PlayQueue.cpp impl/objects/Podcast.cpp impl/objects/PodcastEpisode.cpp - impl/objects/TrackArtistLink.cpp - impl/objects/TrackFeatures.cpp - impl/objects/TrackList.cpp + impl/objects/ReleaseArtistLink.cpp impl/objects/RatedArtist.cpp impl/objects/RatedRelease.cpp impl/objects/RatedTrack.cpp @@ -26,9 +24,12 @@ add_library(lmsdatabase STATIC impl/objects/StarredRelease.cpp impl/objects/StarredTrack.cpp impl/objects/Track.cpp + impl/objects/TrackArtistLink.cpp impl/objects/TrackBookmark.cpp impl/objects/TrackEmbeddedImage.cpp impl/objects/TrackEmbeddedImageLink.cpp + impl/objects/TrackFeatures.cpp + impl/objects/TrackList.cpp impl/objects/TrackLyrics.cpp impl/objects/Types.cpp impl/objects/UIState.cpp diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 639c8f08..e9fcf2c2 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -35,7 +35,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 102 }; + static constexpr Version LMS_DATABASE_VERSION{ 103 }; } VersionInfo::VersionInfo() @@ -1681,6 +1681,31 @@ FROM track)"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bits_per_sample INTEGER"); } + void migrateFromV102(Session& session) + { + // New link to artists from releases + utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_artist_link" ( + "id" integer primary key autoincrement, + "version" integer not null, + "artist_name" text not null, + "artist_sort_name" text not null, + "artist_mbid_matched" boolean not null, + "release_id" bigint, + "artist_id" bigint, + constraint "fk_release_artist_link_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, + constraint "fk_release_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred + ))"); + + // Remove all artist links where the link type is TrackArtistLinkType::ReleaseArtist = 8 + utils::executeCommand(*session.getDboSession(), R"(DELETE FROM track_artist_link WHERE type = 8)"); + + // Remove outdated UI state entries + utils::executeCommand(*session.getDboSession(), R"(DELETE FROM ui_state WHERE item = 'artists_link_type')"); + + // 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) { constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -1759,6 +1784,7 @@ FROM track)"); { 99, migrateFromV99 }, { 100, migrateFromV100 }, { 101, migrateFromV101 }, + { 102, migrateFromV102 }, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 48127a71..f335d98c 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -42,6 +42,7 @@ #include "database/objects/RatedRelease.hpp" #include "database/objects/RatedTrack.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ScanSettings.hpp" #include "database/objects/StarredArtist.hpp" #include "database/objects/StarredRelease.hpp" @@ -94,6 +95,7 @@ namespace lms::db _session.mapClass("rated_release"); _session.mapClass("rated_track"); _session.mapClass("release"); + _session.mapClass("release_artist_link"); _session.mapClass("release_type"); _session.mapClass("scan_settings"); _session.mapClass("starred_artist"); diff --git a/src/libs/database/impl/objects/Artist.cpp b/src/libs/database/impl/objects/Artist.cpp index ab7cbac6..3d9c13c1 100644 --- a/src/libs/database/impl/objects/Artist.cpp +++ b/src/libs/database/impl/objects/Artist.cpp @@ -27,9 +27,7 @@ #include "database/objects/Cluster.hpp" #include "database/objects/Directory.hpp" #include "database/objects/Release.hpp" -// #include "database/objects/StarredArtist.hpp" #include "database/objects/Track.hpp" -// #include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" #include "SqlQuery.hpp" @@ -53,9 +51,8 @@ namespace lms::db if (params.sortMethod == ArtistSortMethod::LastWrittenDesc || params.sortMethod == ArtistSortMethod::AddedDesc || params.writtenAfter.isValid() - || params.linkType + || params.trackArtistLinkType.has_value() || params.track.isValid() - || params.release.isValid() || params.filters.clusters.size() == 1 || params.filters.codec.has_value() || params.filters.mediaLibrary.isValid() @@ -68,7 +65,6 @@ namespace lms::db if (params.sortMethod == ArtistSortMethod::LastWrittenDesc || params.sortMethod == ArtistSortMethod::AddedDesc || params.writtenAfter.isValid() - || params.release.isValid() || params.filters.codec.has_value() || params.filters.mediaLibrary.isValid() || params.filters.label.isValid() @@ -79,9 +75,6 @@ namespace lms::db if (params.writtenAfter.isValid()) query.where("t.file_last_write > ?").bind(params.writtenAfter); - if (params.release.isValid()) - query.where("t.release_id = ?").bind(params.release); - if (params.filters.codec.has_value()) query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); @@ -101,8 +94,11 @@ namespace lms::db } } - if (params.linkType) - query.where("+t_a_l.type = ?").bind(*params.linkType); // Exclude this since the query planner does not do a good job when db is not analyzed + if (params.releaseArtistsOnly) + query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id"); + + if (params.trackArtistLinkType.has_value()) + query.where("+t_a_l.type = ?").bind(*params.trackArtistLinkType); // Exclude this since the query planner does not do a good job when db is not analyzed if (!params.keywords.empty()) { @@ -318,6 +314,7 @@ namespace lms::db RangeResults Artist::findOrphanIds(Session& session, std::optional range) { + // TODO extend with release artists session.checkReadTransaction(); auto query{ session.getDboSession()->query(R"(SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS ( @@ -327,6 +324,12 @@ WHERE NOT EXISTS ( ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id ) +AND NOT EXISTS ( + SELECT 1 + FROM release r + INNER JOIN release_artist_link r_a_l + ON r_a_l.artist_id = a.id + WHERE r.id = r_a_l.release_id) AND NOT EXISTS ( SELECT 1 FROM artist_info ai diff --git a/src/libs/database/impl/objects/Directory.cpp b/src/libs/database/impl/objects/Directory.cpp index 293728c2..47be8b9a 100644 --- a/src/libs/database/impl/objects/Directory.cpp +++ b/src/libs/database/impl/objects/Directory.cpp @@ -42,7 +42,8 @@ namespace lms::db for (std::string_view keyword : params.keywords) query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); - if (params.artist.isValid() + if (params.trackArtist.isValid() + || params.releaseArtist.isValid() || params.release.isValid() || params.medium.isValid()) { @@ -62,12 +63,24 @@ namespace lms::db if (params.release.isValid()) query.where("t.release_id = ?").bind(params.release); - if (params.artist.isValid()) + if (params.releaseArtist.isValid()) { + assert(!params.trackArtist.isValid()); + + query.join("artist a ON a.id = r_a_l.artist_id") + .join("release_artist_link r_a_l ON r_a_l.release_id = t.release_id") + .where("a.id = ?") + .bind(params.releaseArtist); + } + + if (params.trackArtist.isValid()) + { + assert(!params.releaseArtist.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); + .bind(params.trackArtist); if (!params.trackArtistLinkTypes.empty()) { diff --git a/src/libs/database/impl/objects/Listen.cpp b/src/libs/database/impl/objects/Listen.cpp index 3c146aa5..82b472dc 100644 --- a/src/libs/database/impl/objects/Listen.cpp +++ b/src/libs/database/impl/objects/Listen.cpp @@ -52,7 +52,8 @@ namespace lms::db if (params.filters.mediaLibrary.isValid() || params.filters.codec.has_value() || params.filters.label.isValid() - || params.filters.releaseType.isValid()) + || params.filters.releaseType.isValid() + || params.trackArtistLinkType.has_value()) { query.join("track t ON t.id = t_a_l.track_id"); @@ -75,8 +76,11 @@ namespace lms::db } } - if (params.linkType) - query.where("t_a_l.type = ?").bind(*params.linkType); + if (params.releaseArtistsOnly) + query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id"); + + if (params.trackArtistLinkType.has_value()) + query.where("t_a_l.type = ?").bind(params.trackArtistLinkType.value()); if (!params.filters.clusters.empty()) { diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index 4525289d..bd439d1a 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -31,6 +31,7 @@ #include "database/objects/Directory.hpp" #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" @@ -72,7 +73,7 @@ namespace lms::db || params.sortMethod == ReleaseSortMethod::OriginalDateDesc || params.writtenAfter.isValid() || params.dateRange - || params.artist.isValid() + || params.trackArtist.isValid() || params.filters.clusters.size() == 1 || params.filters.mediaLibrary.isValid() || params.filters.codec.has_value() @@ -141,13 +142,22 @@ namespace lms::db .bind(SyncState::PendingRemove); } - if (params.artist.isValid() + if (params.artist.isValid()) + { + assert(!params.trackArtist.isValid()); + query.join("release_artist_link r_a_l ON r_a_l.release_id = r.id"); + query.where("r_a_l.artist_id = ?").bind(params.artist); + } + + if (params.trackArtist.isValid() || params.sortMethod == ReleaseSortMethod::ArtistNameThenName) { + assert(!params.artist.isValid()); + query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id"); - if (params.artist.isValid()) - query.where("t_a_l.artist_id = ?").bind(params.artist); + if (params.trackArtist.isValid()) + query.where("t_a_l.artist_id = ?").bind(params.trackArtist); if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName) query.join("artist a ON a.id = t_a_l.artist_id"); @@ -168,30 +178,6 @@ namespace lms::db } 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 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 (t_a_l.artist_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.filters.clusters.size() == 1) @@ -272,7 +258,7 @@ namespace lms::db } template - Wt::Dbo::Query createArtistQuery(Wt::Dbo::Session& session, std::string_view itemToSelect, ReleaseId releaseId, TrackArtistLinkType linkType) + Wt::Dbo::Query createTrackArtistQuery(Wt::Dbo::Session& session, std::string_view itemToSelect, ReleaseId releaseId, TrackArtistLinkType linkType) { auto query{ session.query("SELECT " + std::string{ itemToSelect } + " from artist a") .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") @@ -700,19 +686,50 @@ namespace lms::db return res; } - std::vector Release::getArtists(TrackArtistLinkType linkType) const + std::vector> Release::getArtists() const { assert(session()); - const auto query{ createArtistQuery>(*session(), "a", getId(), linkType) }; + auto query{ session()->query>("SELECT a from artist a") }; + query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id"); + query.where("r_a_l.release_id = ?").bind(getId()); + query.groupBy("a.id"); + return utils::fetchQueryResults(query); } - std::vector Release::getArtistIds(TrackArtistLinkType linkType) const + bool Release::hasArtist(ArtistId artistId) const { assert(session()); - const auto query{ createArtistQuery(*session(), "a.id", getId(), linkType) }; + auto query{ session()->query("SELECT COUNT(1) FROM release_artist_link r_a_l") }; + query.where("r_a_l.release_id = ?").bind(getId()); + query.where("r_a_l.artist_id = ?").bind(artistId); + + return utils::fetchQuerySingleResult(query) > 0; + } + + std::vector Release::getTrackArtists(TrackArtistLinkType type) const + { + assert(session()); + + const auto query{ createTrackArtistQuery>(*session(), "a", getId(), type) }; + return utils::fetchQueryResults(query); + } + + void Release::visitTrackArtists(TrackArtistLinkType type, std::function&)> visitor) const + { + assert(session()); + + const auto query{ createTrackArtistQuery>(*session(), "a", getId(), type) }; + return utils::forEachQueryResult(query, visitor); + } + + std::vector Release::getTrackArtistIds(TrackArtistLinkType linkType) const + { + assert(session()); + + const auto query{ createTrackArtistQuery(*session(), "a.id", getId(), linkType) }; return utils::fetchQueryResults(query); } @@ -765,6 +782,36 @@ namespace lms::db return utils::fetchQueryResults(query); } + std::vector> Release::getArtistLinks() const + { + return utils::fetchQueryResults(_releaseArtistLinks.find()); + } + + void Release::visitArtistLinks(const std::function& visitor) const + { + return utils::forEachQueryResult(_releaseArtistLinks.find(), visitor); + } + + void Release::visitTrackArtistLinks(TrackArtistLinkType linkType, const std::function& artistLink)>& visitor) const + { + auto query{ session()->query>("SELECT t_a_l from track_artist_link t_a_l") }; + query.join("track t ON t.id = t_a_l.track_id"); + query.where("t.release_id = ?").bind(getId()); + query.where("t_a_l.type = ?").bind(linkType); + + utils::forEachQueryResult(query, visitor); + } + + void Release::clearArtistLinks() + { + _releaseArtistLinks.clear(); + } + + void Release::addArtistLink(const ObjectPtr& artistLink) + { + _releaseArtistLinks.insert(getDboPtr(artistLink)); + } + void Release::clearLabels() { _labels.clear(); @@ -803,7 +850,7 @@ namespace lms::db bool Release::hasVariousArtists() const { // TODO optimize - return getArtists().size() > 1; + return getTrackArtists().size() > 1; } std::size_t Release::getTrackCount() const diff --git a/src/libs/database/impl/objects/ReleaseArtistLink.cpp b/src/libs/database/impl/objects/ReleaseArtistLink.cpp new file mode 100644 index 00000000..6c508c4c --- /dev/null +++ b/src/libs/database/impl/objects/ReleaseArtistLink.cpp @@ -0,0 +1,157 @@ +/* + * 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 . + */ + +#include "database/objects/ReleaseArtistLink.hpp" + +#include + +#include "core/ILogger.hpp" +#include "database/Session.hpp" +#include "database/objects/Artist.hpp" +#include "database/objects/Release.hpp" +#include "database/objects/Track.hpp" + +#include "Utils.hpp" +#include "traits/IdTypeTraits.hpp" + +DBO_INSTANTIATE_TEMPLATES(lms::db::ReleaseArtistLink) + +namespace lms::db +{ + namespace + { + Wt::Dbo::Query> createQuery(Session& session, const ReleaseArtistLink::FindParameters& params) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>("SELECT r_a_l FROM release_artist_link r_a_l") }; + + if (params.release.isValid()) + query.where("r_a_l.release_id = ?").bind(params.release); + + if (params.artist.isValid()) + query.where("r_a_l.artist_id = ?").bind(params.artist); + + if (params.sortMethod == ReleaseArtistLinkSortMethod::OriginalDateDesc) + query.join("track t ON t.release_id = r_a_l.release_id"); + + if (params.mbidMatched) + query.where("r_a_l.artist_mbid_matched = ?").bind(*params.mbidMatched); + + switch (params.sortMethod) + { + case ReleaseArtistLinkSortMethod::None: + break; + case ReleaseArtistLinkSortMethod::OriginalDateDesc: + query.orderBy("COALESCE(t.original_date, t.date) DESC"); + break; + } + + return query; + } + } // namespace + + ReleaseArtistLink::ReleaseArtistLink(const ObjectPtr& release, const ObjectPtr& artist, bool artistMBIDMatched) + : _artistMBIDMatched{ artistMBIDMatched } + , _release{ getDboPtr(release) } + , _artist{ getDboPtr(artist) } + { + } + + ReleaseArtistLink::pointer ReleaseArtistLink::create(Session& session, const ObjectPtr& release, const ObjectPtr& artist, bool artistMBIDMatched) + { + session.checkWriteTransaction(); + return session.getDboSession()->add(std::make_unique(release, artist, artistMBIDMatched)); + } + + std::size_t ReleaseArtistLink::getCount(Session& session) + { + session.checkReadTransaction(); + + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_artist_link")); + } + + ReleaseArtistLink::pointer ReleaseArtistLink::find(Session& session, ReleaseArtistLinkId id) + { + session.checkReadTransaction(); + return utils::fetchQuerySingleResult(session.getDboSession()->query>("SELECT r_a_l from release_artist_link r_a_l").where("r_a_l.id = ?").bind(id)); + } + + void ReleaseArtistLink::find(Session& session, const FindParameters& params, std::function func) + { + auto query{ createQuery(session, params) }; + utils::forEachQueryRangeResult(query, params.range, func); + } + + void ReleaseArtistLink::findArtistNameNoLongerMatch(Session& session, std::optional range, const std::function& func) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>("SELECT r_a_l from release_artist_link r_a_l") }; + query.join("artist a ON r_a_l.artist_id = a.id"); + query.where("r_a_l.artist_mbid_matched = FALSE"); + query.where("r_a_l.artist_name <> a.name"); + + utils::applyRange(query, range); + utils::forEachQueryResult(query, [&](const ReleaseArtistLink::pointer& link) { + func(link); + }); + } + + void ReleaseArtistLink::findWithArtistNameAmbiguity(Session& session, std::optional range, bool allowArtistMBIDFallback, const std::function& func) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->query>("SELECT r_a_l from release_artist_link r_a_l") }; + query.join("artist a ON r_a_l.artist_id = a.id"); + query.where("r_a_l.artist_mbid_matched = FALSE"); + if (!allowArtistMBIDFallback) + { + query.where("a.mbid <> ''"); + } + else + { + query.where(R"( + (a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid)) + OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))"); + } + + utils::applyRange(query, range); + utils::forEachQueryResult(query, [&](const ReleaseArtistLink::pointer& link) { + func(link); + }); + } + + void ReleaseArtistLink::setArtist(ObjectPtr artist) + { + _artist = getDboPtr(artist); + } + + void ReleaseArtistLink::setArtistName(std::string_view artistName) + { + _artistName.assign(artistName, 0, Artist::maxNameLength); + LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'"); + } + + void ReleaseArtistLink::setArtistSortName(std::string_view artistSortName) + { + _artistSortName.assign(artistSortName, 0, Artist::maxNameLength); + LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'"); + } +} // namespace lms::db diff --git a/src/libs/database/impl/objects/Track.cpp b/src/libs/database/impl/objects/Track.cpp index bd7acaac..60c5d2a1 100644 --- a/src/libs/database/impl/objects/Track.cpp +++ b/src/libs/database/impl/objects/Track.cpp @@ -728,6 +728,27 @@ namespace lms::db return utils::fetchQueryResults(_trackArtistLinks.find()); } + void Track::visitArtistLinks(const std::function& artistLink)>& visitor) const + { + utils::forEachQueryResult(_trackArtistLinks.find(), visitor); + } + + std::vector> Track::getArtistLinks(TrackArtistLinkType type) const + { + std::vector> links; + visitArtistLinks(type, [&links](const ObjectPtr& artistLink) { links.push_back(artistLink); }); + return links; + } + + void Track::visitArtistLinks(TrackArtistLinkType type, const std::function& artistLink)>& visitor) const + { + auto query{ session()->query>("SELECT t_a_l from track_artist_link t_a_l") }; + query.where("t_a_l.track_id = ?").bind(getId()); + query.where("t_a_l.type = ?").bind(type); + + return utils::forEachQueryResult(query, visitor); + } + std::vector> Track::getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const { assert(self()); diff --git a/src/libs/database/impl/objects/TrackArtistLink.cpp b/src/libs/database/impl/objects/TrackArtistLink.cpp index 462f60f7..3366d6bc 100644 --- a/src/libs/database/impl/objects/TrackArtistLink.cpp +++ b/src/libs/database/impl/objects/TrackArtistLink.cpp @@ -88,10 +88,12 @@ namespace lms::db { session.checkWriteTransaction(); - TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique(track, artist, type, subType, artistMBIDMatched)) }; - session.getDboSession()->flush(); + return session.getDboSession()->add(std::make_unique(track, artist, type, subType, artistMBIDMatched)); + } - return res; + TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, bool artistMBIDMatched) + { + return create(session, track, artist, type, std::string_view{}, artistMBIDMatched); } std::size_t TrackArtistLink::getCount(Session& session) @@ -101,11 +103,6 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM track_artist_link")); } - TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, bool artistMBIDMatched) - { - return create(session, track, artist, type, std::string_view{}, artistMBIDMatched); - } - TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id) { session.checkReadTransaction(); @@ -125,10 +122,10 @@ namespace lms::db }); } - void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function& func) + void TrackArtistLink::find(Session& session, const FindParameters& params, const std::function& func) { - auto query{ createQuery(session, parameters) }; - utils::forEachQueryRangeResult(query, parameters.range, func); + auto query{ createQuery(session, params) }; + utils::forEachQueryRangeResult(query, params.range, func); } core::EnumSet TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId) diff --git a/src/libs/database/impl/objects/Types.cpp b/src/libs/database/impl/objects/Types.cpp index 2ae8bede..f1f698fd 100644 --- a/src/libs/database/impl/objects/Types.cpp +++ b/src/libs/database/impl/objects/Types.cpp @@ -43,8 +43,6 @@ namespace lms::db return "performer"; case TrackArtistLinkType::Producer: return "producer"; - case TrackArtistLinkType::ReleaseArtist: - return "albumartist"; case TrackArtistLinkType::Remixer: return "remixer"; case TrackArtistLinkType::Writer: diff --git a/src/libs/database/include/database/objects/Artist.hpp b/src/libs/database/include/database/objects/Artist.hpp index e6f82723..b12ed211 100644 --- a/src/libs/database/include/database/objects/Artist.hpp +++ b/src/libs/database/include/database/objects/Artist.hpp @@ -62,15 +62,15 @@ namespace lms::db struct FindParameters { Filters filters; - std::vector keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) - std::optional linkType; // if set, only artists that have produced at least one track with this link type + std::vector keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) + bool releaseArtistsOnly{}; // if set, only release artists + std::optional trackArtistLinkType; // if set, only artists that have produced at least one track with this link type ArtistSortMethod sortMethod{ ArtistSortMethod::None }; std::optional range; Wt::WDateTime writtenAfter; UserId starringUser; // only artists starred by this user std::optional feedbackBackend; // and for this feedback backend TrackId track; // artists involved in this track - ReleaseId release; // artists involved in this release FindParameters& setFilters(const Filters& _filters) { @@ -82,9 +82,14 @@ namespace lms::db keywords = _keywords; return *this; } - FindParameters& setLinkType(std::optional _linkType) + FindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly) { - linkType = _linkType; + releaseArtistsOnly = _releaseArtistsOnly; + return *this; + } + FindParameters& setTrackArtistLinkType(std::optional _trackArtistLinkType) + { + trackArtistLinkType = _trackArtistLinkType; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) @@ -113,11 +118,6 @@ namespace lms::db track = _track; return *this; } - FindParameters& setRelease(ReleaseId _release) - { - release = _release; - return *this; - } }; Artist() = default; diff --git a/src/libs/database/include/database/objects/Directory.hpp b/src/libs/database/include/database/objects/Directory.hpp index dd8ac918..cbc46971 100644 --- a/src/libs/database/include/database/objects/Directory.hpp +++ b/src/libs/database/include/database/objects/Directory.hpp @@ -53,10 +53,11 @@ namespace lms::db { std::optional range; std::vector keywords; // if non empty, name must match all of these keywords - ArtistId artist; // only directoies that involve this artist + ArtistId trackArtist; // only directories that involve this track artist + core::EnumSet trackArtistLinkTypes; // and for these link types MediumId medium; // only directories that involve this medium ReleaseId release; // only directories that involve this release - core::EnumSet trackArtistLinkTypes; // and for these link types + ArtistId releaseArtist; // only directories that involve this release artist DirectoryId parentDirectory; // If set, directories that have this parent bool withNoTrack{}; // If set, directories that do not contain any track MediaLibraryId mediaLibrary; // If set, directories in this library @@ -72,12 +73,18 @@ namespace lms::db keywords = _keywords; return *this; } - FindParameters& setArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}) + FindParameters& setTrackArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}) { - artist = _artist; + trackArtist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } + FindParameters& setReleaseArtist(ArtistId _artist) + { + releaseArtist = _artist; + return *this; + } + FindParameters& setMedium(MediumId _medium) { medium = _medium; diff --git a/src/libs/database/include/database/objects/Listen.hpp b/src/libs/database/include/database/objects/Listen.hpp index 13d6f77a..cf63e24c 100644 --- a/src/libs/database/include/database/objects/Listen.hpp +++ b/src/libs/database/include/database/objects/Listen.hpp @@ -124,11 +124,17 @@ namespace lms::db struct ArtistStatsFindParameters : public StatsFindParameters { - std::optional linkType; // if set, only artists that have produced at least one track with this link type + std::optional trackArtistLinkType; // if set, only artists that have produced at least one track with this link type + bool releaseArtistsOnly{}; - ArtistStatsFindParameters& setLinkType(std::optional _linkType) + ArtistStatsFindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly) { - linkType = _linkType; + releaseArtistsOnly = _releaseArtistsOnly; + return *this; + } + ArtistStatsFindParameters& setTrackArtistLinkType(std::optional _trackArtistLinkType) + { + trackArtistLinkType = _trackArtistLinkType; return *this; } }; diff --git a/src/libs/database/include/database/objects/Release.hpp b/src/libs/database/include/database/objects/Release.hpp index 360b20d7..6f6c1035 100644 --- a/src/libs/database/include/database/objects/Release.hpp +++ b/src/libs/database/include/database/objects/Release.hpp @@ -56,8 +56,10 @@ namespace lms::db class ClusterType; class Medium; class Release; + class ReleaseArtistLink; class Session; class Track; + class TrackArtistLink; class User; class Country final : public Object @@ -167,15 +169,15 @@ namespace lms::db std::optional range; Wt::WDateTime writtenAfter; std::optional dateRange; - UserId starringUser; // only releases starred by this user - std::optional feedbackBackend; // and for this backend - ArtistId artist; // only releases that involved this user - core::EnumSet trackArtistLinkTypes; // and for these link types - core::EnumSet excludedTrackArtistLinkTypes; // but not for these link types - std::string releaseType; // If set, releases that has this release type - std::optional releaseGroupMBID; // If set, releases that belong to this release group - DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory) - DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory) + UserId starringUser; // only releases starred by this user + std::optional feedbackBackend; // and for this backend + ArtistId artist; // only releases by this release artist + ArtistId trackArtist; // only releases that involved this track artist + core::EnumSet trackArtistLinkTypes; // and for these link types, if set + std::string releaseType; // If set, releases that has this release type + std::optional releaseGroupMBID; // If set, releases that belong to this release group + DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory) + DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory) FindParameters& setFilters(const Filters& _filters) { @@ -218,11 +220,15 @@ namespace lms::db feedbackBackend = _feedbackBackend; return *this; } - FindParameters& setArtist(ArtistId _artist, core::EnumSet _trackArtistLinkTypes = {}, core::EnumSet _excludedTrackArtistLinkTypes = {}) + FindParameters& setArtist(ArtistId _artist) { artist = _artist; + return *this; + } + FindParameters& setTrackArtist(ArtistId _trackArtist, core::EnumSet _trackArtistLinkTypes = {}) + { + trackArtist = _trackArtist; trackArtistLinkTypes = _trackArtistLinkTypes; - excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes; return *this; } FindParameters& setReleaseType(std::string_view _releaseType) @@ -305,6 +311,9 @@ namespace lms::db ObjectPtr getPreferredArtwork() const; ArtworkId getPreferredArtworkId() const; std::vector> getMediums() const; + std::vector> getArtistLinks() const; + void visitArtistLinks(const std::function& artistLink)>& visitor) const; + void visitTrackArtistLinks(TrackArtistLinkType linkType, const std::function& artistLink)>& visitor) const; // Setters void setName(std::string_view name) { _name = name; } @@ -313,6 +322,8 @@ namespace lms::db void setGroupMBID(const std::optional& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; } void setTotalDisc(std::optional totalDisc) { _totalDisc = totalDisc; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } + void clearArtistLinks(); + void addArtistLink(const ObjectPtr& artistLink); void setCompilation(bool value) { _isCompilation = value; } void clearLabels(); void clearCountries(); @@ -325,9 +336,11 @@ namespace lms::db void setPreferredArtwork(ObjectPtr artwork); // Get the artists of this release - std::vector> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; - std::vector getArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; - std::vector> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } + std::vector> getArtists() const; + bool hasArtist(ArtistId artistId) const; + std::vector> getTrackArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; + void visitTrackArtists(TrackArtistLinkType type, std::function&)> visitor) const; + std::vector getTrackArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; bool hasVariousArtists() const; std::vector getSimilarReleases(std::optional offset = {}, std::optional count = {}) const; @@ -345,6 +358,7 @@ namespace lms::db Wt::Dbo::field(a, _comment, "comment"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); + Wt::Dbo::hasMany(a, _releaseArtistLinks, Wt::Dbo::ManyToOne, "release"); Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade); @@ -372,6 +386,7 @@ namespace lms::db std::string _comment; Wt::Dbo::collection> _tracks; + Wt::Dbo::collection> _releaseArtistLinks; Wt::Dbo::ptr _preferredArtwork; Wt::Dbo::collection> _labels; Wt::Dbo::collection> _releaseTypes; diff --git a/src/libs/database/include/database/objects/ReleaseArtistLink.hpp b/src/libs/database/include/database/objects/ReleaseArtistLink.hpp new file mode 100644 index 00000000..76542573 --- /dev/null +++ b/src/libs/database/include/database/objects/ReleaseArtistLink.hpp @@ -0,0 +1,127 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include + +#include "database/IdType.hpp" +#include "database/Object.hpp" +#include "database/Types.hpp" +#include "database/objects/ArtistId.hpp" +#include "database/objects/ReleaseId.hpp" +#include "database/objects/Types.hpp" + +LMS_DECLARE_IDTYPE(ReleaseArtistLinkId) + +namespace lms::db +{ + class Artist; + class Session; + class Release; + + class ReleaseArtistLink final : public Object + { + public: + struct FindParameters + { + std::optional range; + ArtistId artist; // if set, links involved with this artist + ReleaseId release; // if set, artists involved in this release + std::optional mbidMatched; + ReleaseArtistLinkSortMethod sortMethod{ ReleaseArtistLinkSortMethod::None }; + + FindParameters& setRange(std::optional _range) + { + range = _range; + return *this; + } + FindParameters& setArtist(ArtistId _artist) + { + artist = _artist; + return *this; + } + FindParameters& setRelease(ReleaseId _release) + { + release = _release; + return *this; + } + FindParameters& setMBIDMatched(std::optional _mbidMatched) + { + mbidMatched = _mbidMatched; + return *this; + } + FindParameters& setSortMethod(ReleaseArtistLinkSortMethod _method) + { + sortMethod = _method; + return *this; + } + }; + + ReleaseArtistLink() = default; + ReleaseArtistLink(const ObjectPtr& release, const ObjectPtr& artist, bool artistMBIDMatched); + + static pointer find(Session& session, ReleaseArtistLinkId linkId); + static void find(Session& session, const FindParameters& params, std::function func); + static std::size_t getCount(Session& session); + + static void findArtistNameNoLongerMatch(Session& session, std::optional range, const std::function& func); + static void findWithArtistNameAmbiguity(Session& session, std::optional range, bool allowArtistMBIDFallback, const std::function& func); + + // accessors + ObjectPtr getRelease() const { return _release; } + ObjectPtr getArtist() const { return _artist; } + ArtistId getArtistId() const { return _artist.id(); } + std::string_view getArtistName() const { return _artistName; } + std::string_view getArtistSortName() const { return _artistSortName; } + bool isArtistMBIDMatched() const { return _artistMBIDMatched; } + + // setters + void setArtist(ObjectPtr artist); + void setArtistName(std::string_view artistName); + void setArtistSortName(std::string_view artistSortName); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _artistName, "artist_name"); + Wt::Dbo::field(a, _artistSortName, "artist_sort_name"); + Wt::Dbo::field(a, _artistMBIDMatched, "artist_mbid_matched"); + + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + } + + private: + friend class Session; + static pointer create(Session& session, const ObjectPtr& release, const ObjectPtr& artist, bool artistMBIDMatched); + + TrackArtistLinkType _type{ TrackArtistLinkType::Artist }; + std::string _subType; + std::string _artistName; // as it was in the tags + std::string _artistSortName; // as it was in the tags + bool _artistMBIDMatched{}; + + Wt::Dbo::ptr _release; + Wt::Dbo::ptr _artist; + }; +} // namespace lms::db diff --git a/src/libs/database/include/database/objects/Track.hpp b/src/libs/database/include/database/objects/Track.hpp index e5fc2efa..4a43e8b6 100644 --- a/src/libs/database/include/database/objects/Track.hpp +++ b/src/libs/database/include/database/objects/Track.hpp @@ -307,6 +307,10 @@ namespace lms::db std::vector> getArtists(core::EnumSet artistLinkTypes) const; // no type means all std::vector getArtistIds(core::EnumSet artistLinkTypes) const; // no type means all std::vector> getArtistLinks() const; + void visitArtistLinks(const std::function& artistLink)>& visitor) const; + std::vector> getArtistLinks(TrackArtistLinkType type) const; + void visitArtistLinks(TrackArtistLinkType type, const std::function& artistLink)>& visitor) const; + ReleaseId getReleaseId() const { return _release.id(); } ObjectPtr getRelease() const { return _release; } MediumId getMediumId() const { return _medium.id(); } @@ -320,7 +324,7 @@ namespace lms::db ObjectPtr getPreferredMediaArtwork() const; ArtworkId getPreferredMediaArtworkId() const; - std::vector>> getClusterGroups(const std::vector& clusterTypes, std::size_t size) const; + std::vector>> getClusterGroups(const std::vector& clusterTypeIds, std::size_t size) const; template void persist(Action& a) diff --git a/src/libs/database/include/database/objects/TrackArtistLink.hpp b/src/libs/database/include/database/objects/TrackArtistLink.hpp index e10ec355..cf730c03 100644 --- a/src/libs/database/include/database/objects/TrackArtistLink.hpp +++ b/src/libs/database/include/database/objects/TrackArtistLink.hpp @@ -97,11 +97,9 @@ namespace lms::db TrackArtistLink(const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched); static void find(Session& session, TrackId trackId, const std::function&)>& func); - static void find(Session& session, const FindParameters& parameters, const std::function& func); + static void find(Session& session, const FindParameters& params, const std::function& func); static pointer find(Session& session, TrackArtistLinkId linkId); static std::size_t getCount(Session& session); - static pointer create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched = false); - static pointer create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, bool artistMBIDMatched = false); static core::EnumSet findUsedTypes(Session& session, ArtistId _artist); static void findArtistNameNoLongerMatch(Session& session, std::optional range, const std::function& func); static void findWithArtistNameAmbiguity(Session& session, std::optional range, bool allowArtistMBIDFallback, const std::function& func); @@ -109,6 +107,7 @@ namespace lms::db // accessors ObjectPtr getTrack() const { return _track; } ObjectPtr getArtist() const { return _artist; } + ArtistId getArtistId() const { return _artist.id(); } TrackArtistLinkType getType() const { return _type; } std::string_view getSubType() const { return _subType; } std::string_view getArtistName() const { return _artistName; } @@ -134,6 +133,10 @@ namespace lms::db } private: + friend class Session; + static pointer create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched = false); + static pointer create(Session& session, const ObjectPtr& track, const ObjectPtr& artist, TrackArtistLinkType type, bool artistMBIDMatched = false); + TrackArtistLinkType _type{ TrackArtistLinkType::Artist }; std::string _subType; std::string _artistName; // as it was in the tags diff --git a/src/libs/database/include/database/objects/Types.hpp b/src/libs/database/include/database/objects/Types.hpp index 47953b83..77fbea2d 100644 --- a/src/libs/database/include/database/objects/Types.hpp +++ b/src/libs/database/include/database/objects/Types.hpp @@ -86,6 +86,12 @@ namespace lms::db PubDateDesc, }; + enum class ReleaseArtistLinkSortMethod + { + None, + OriginalDateDesc, + }; + enum class ReleaseSortMethod { None, @@ -165,7 +171,7 @@ namespace lms::db Mixer = 5, Performer = 6, Producer = 7, - ReleaseArtist = 8, + // ReleaseArtist = 8, Remixer = 9, Writer = 10, }; diff --git a/src/libs/database/test/Artist.cpp b/src/libs/database/test/Artist.cpp index 3986e74a..c58acccb 100644 --- a/src/libs/database/test/Artist.cpp +++ b/src/libs/database/test/Artist.cpp @@ -21,6 +21,7 @@ #include "database/objects/Artwork.hpp" #include "database/objects/Image.hpp" +#include "database/objects/ReleaseArtistLink.hpp" namespace lms::db::tests { @@ -98,10 +99,10 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; track2a.get().modify()->setMediaLibrary(library.get()); track2b.get().modify()->setMediaLibrary(library.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2a.get(), artist2.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2b.get(), artist2.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track3.get(), artist3.get(), TrackArtistLinkType::Artist); + session.create(track1.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track2a.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track2b.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track3.get(), artist3.get(), TrackArtistLinkType::Artist); } { @@ -243,7 +244,7 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; track.get().modify()->setName("MyTrackName"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -258,13 +259,39 @@ namespace lms::db::tests ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist.getId()); + { + bool visited{}; + track->visitArtistLinks([&](const db::TrackArtistLink::pointer& artistLink) { + visited = true; + EXPECT_EQ(artistLink->getArtistId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } + + { + bool visited{}; + track->visitArtistLinks(TrackArtistLinkType::Artist, [&](const db::TrackArtistLink::pointer& artistLink) { + visited = true; + EXPECT_EQ(artistLink->getArtistId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } + + { + bool visited{}; + track->visitArtistLinks(TrackArtistLinkType::Conductor, [&](const db::TrackArtistLink::pointer&) { + visited = true; + }); + EXPECT_FALSE(visited); + } + ASSERT_EQ(track->getArtistLinks().size(), 1); auto artistLink{ track->getArtistLinks().front() }; EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1); - EXPECT_EQ(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).size(), 0); + EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Composer }).size(), 0); EXPECT_EQ(track->getArtists({}).size(), 1); } @@ -276,7 +303,7 @@ namespace lms::db::tests EXPECT_EQ(artists.front(), artist.getId()); ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1); - EXPECT_EQ(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).size(), 0); + EXPECT_EQ(track->getArtistIds({ TrackArtistLinkType::Composer }).size(), 0); EXPECT_EQ(track->getArtistIds({}).size(), 1); } @@ -302,6 +329,32 @@ namespace lms::db::tests ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); + } + } + + TEST_F(DatabaseFixture, Artist_singleRelease) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedArtist artist{ session, "MyArtist" }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artist::findOrphanIds(session).results, std::vector{ artist.getId() }); + } + + { + auto transaction{ session.createWriteTransaction() }; + session.create(release.get(), artist.get(), false); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0); + } } TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary) @@ -315,7 +368,7 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; track.get().modify()->setName("MyTrackName"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); track.get().modify()->setMediaLibrary(library.get()); } { @@ -344,9 +397,9 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Producer); + session.create(track.get(), artist.get(), TrackArtistLinkType::Writer); } { @@ -357,10 +410,12 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).results.size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).results.size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).results.size(), 1); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Composer)).results.size(), 0); + + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); } { @@ -370,7 +425,7 @@ namespace lms::db::tests ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist.getId()); - artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist }); + artists = track->getArtists({ TrackArtistLinkType::Producer }); ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist.getId()); @@ -379,7 +434,7 @@ namespace lms::db::tests auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId())) }; EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Producer })); EXPECT_EQ(tracks.results.size(), 1); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })); EXPECT_EQ(tracks.results.size(), 1); @@ -393,7 +448,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; core::EnumSet types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; - EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); + EXPECT_TRUE(types.contains(TrackArtistLinkType::Producer)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); @@ -416,7 +471,7 @@ namespace lms::db::tests }; EXPECT_TRUE(containsType(TrackArtistLinkType::Artist)); - EXPECT_TRUE(containsType(TrackArtistLinkType::ReleaseArtist)); + EXPECT_TRUE(containsType(TrackArtistLinkType::Producer)); EXPECT_TRUE(containsType(TrackArtistLinkType::Writer)); } } @@ -431,8 +486,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist2.get(), TrackArtistLinkType::Artist); } { @@ -450,7 +505,7 @@ namespace lms::db::tests EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2); - EXPECT_EQ(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).size(), 0); + EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Arranger }).size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); } @@ -466,13 +521,13 @@ namespace lms::db::tests ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track->getId()); - tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist })); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Arranger })); EXPECT_EQ(tracks.results.size(), 0); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist })); EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist })); + tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Arranger })); EXPECT_EQ(tracks.results.size(), 0); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); @@ -518,6 +573,27 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, Artist_findByReleaseArtistLinks) + { + ScopedRelease release{ session, "release" }; + ScopedArtist artist{ session, "artist" }; + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0); + } + + { + auto transaction{ session.createWriteTransaction() }; + session.create(release.get(), artist.get(), false); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 1); + } + } + TEST_F(DatabaseFixture, Artist_findByCodec) { ScopedArtist artist1{ session, "A" }; @@ -527,8 +603,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track1.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track2.get(), artist2.get(), TrackArtistLinkType::Artist); track1.get().modify()->setCodec(core::media::Codec::MP3); track2.get().modify()->setCodec(core::media::Codec::FLAC); @@ -554,7 +630,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; artist.get().modify()->setSortName("ZZZ"); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -768,8 +844,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track1.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track2.get(), artist.get(), TrackArtistLinkType::Artist); track1.get().modify()->setRelease(release.get()); } @@ -783,51 +859,6 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Artist_findByRelease) - { - ScopedArtist artist{ session, "artist" }; - ScopedTrack track1{ session }; - ScopedTrack track2{ session }; - ScopedRelease release{ session, "MyRelease" }; - - { - auto transaction{ session.createReadTransaction() }; - const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) }; - EXPECT_EQ(artists.results.size(), 0); - } - - { - auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); - } - - { - auto transaction{ session.createReadTransaction() }; - const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) }; - EXPECT_EQ(artists.results.size(), 0); - } - - { - auto transaction{ session.createWriteTransaction() }; - track1.get().modify()->setRelease(release.get()); - track2.get().modify()->setRelease(release.get()); - } - - { - auto transaction{ session.createReadTransaction() }; - const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) }; - ASSERT_EQ(artists.results.size(), 1); - EXPECT_EQ(artists.results.front(), artist.getId()); - } - - { - auto transaction{ session.createReadTransaction() }; - const std::size_t count{ Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())) }; - EXPECT_EQ(count, 1); - } - } - TEST_F(DatabaseFixture, Artist_artwork) { ScopedImage image{ session, "/image1.jpg" }; @@ -875,11 +906,11 @@ namespace lms::db::tests trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); - TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); - TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); + session.create(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); + session.create(trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); + session.create(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); + session.create(trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); + session.create(trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); } { @@ -915,11 +946,11 @@ namespace lms::db::tests trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); trackA2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); - TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); - TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); + session.create(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); + session.create(trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); + session.create(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); + session.create(trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); + session.create(trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); } { @@ -976,16 +1007,16 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; { - auto link{ TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; + auto link{ session.create(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; link.modify()->setArtistName("ArtistA"); } { - auto link{ TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; + auto link{ session.create(trackA2.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; link.modify()->setArtistName("AlternateArtistA"); } { - auto link{ TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist, true) }; + auto link{ session.create(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist, true) }; link.modify()->setArtistName("ArtistB"); } } diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 3bbcf2bb..a5cef6da 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(test-database RatedRelease.cpp RatedTrack.cpp Release.cpp + ReleaseArtistLink.cpp ScanSettings.cpp StarredArtist.cpp StarredRelease.cpp diff --git a/src/libs/database/test/Cluster.cpp b/src/libs/database/test/Cluster.cpp index 20019ccb..9a0b49c9 100644 --- a/src/libs/database/test/Cluster.cpp +++ b/src/libs/database/test/Cluster.cpp @@ -445,7 +445,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; + auto trackArtistLink{ session.create(track.get(), artist.get(), TrackArtistLinkType::Artist) }; cluster1.get().modify()->addTrack(track.get()); } @@ -505,8 +505,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Mixer); cluster.get().modify()->addTrack(track.get()); } @@ -544,7 +544,7 @@ namespace lms::db::tests tracks.emplace_back(session); auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); + session.create(tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); for (auto& cluster : clusters) cluster.get().modify()->addTrack(tracks.back().get()); @@ -654,7 +654,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); track.get().modify()->setRelease(release.get()); cluster.get().modify()->addTrack(track.get()); } @@ -675,11 +675,11 @@ namespace lms::db::tests ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster.getId() }))); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -697,7 +697,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; + auto trackArtistLink{ session.create(track.get(), artist.get(), TrackArtistLinkType::Artist) }; track.get().modify()->setRelease(release.get()); cluster1.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get()); @@ -706,11 +706,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId(), cluster2.getId() }))); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list{ cluster1.getId(), cluster2.getId() }))); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -822,10 +822,10 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; if (i < 5) - TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist); else { - TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist); cluster2.get().modify()->addTrack(tracks.back().get()); } @@ -835,7 +835,7 @@ namespace lms::db::tests tracks.emplace_back(session); { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); + session.create(tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); cluster2.get().modify()->addTrack(tracks.back().get()); } @@ -855,12 +855,12 @@ namespace lms::db::tests } { - auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::ReleaseArtist }) }; + auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Lyricist }) }; EXPECT_EQ(artists.results.size(), 0); } { - auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }) }; + auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::Lyricist }) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist2.getId()); } diff --git a/src/libs/database/test/DatabaseTest.cpp b/src/libs/database/test/DatabaseTest.cpp index 5454f718..ac9ec205 100644 --- a/src/libs/database/test/DatabaseTest.cpp +++ b/src/libs/database/test/DatabaseTest.cpp @@ -36,7 +36,7 @@ namespace lms::db::tests auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); + session.create(tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); tracks.back().get().modify()->setRelease(release.get()); } @@ -49,7 +49,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); @@ -67,18 +67,18 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; + auto trackArtistLink{ session.create(track.get(), artist.get(), TrackArtistLinkType::Artist) }; track.get().modify()->setRelease(release.get()); } { auto transaction{ session.createWriteTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - auto artists{ release->getArtists() }; + auto artists{ release->getTrackArtists() }; ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.front()->getId(), artist.getId()); } diff --git a/src/libs/database/test/Listen.cpp b/src/libs/database/test/Listen.cpp index 4a79b015..ddc27c96 100644 --- a/src/libs/database/test/Listen.cpp +++ b/src/libs/database/test/Listen.cpp @@ -18,6 +18,7 @@ */ #include "database/objects/Listen.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "Common.hpp" @@ -170,7 +171,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track2.get(), artist1.get(), TrackArtistLinkType::Artist); } { @@ -200,7 +201,7 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); - params.setLinkType(TrackArtistLinkType::Producer); + params.setTrackArtistLinkType(TrackArtistLinkType::Producer); auto artists{ Listen::getTopArtists(session, params) }; EXPECT_EQ(artists.results.size(), 0); @@ -235,8 +236,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track1.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track2.get(), artist2.get(), TrackArtistLinkType::Artist); } { @@ -304,7 +305,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -362,7 +363,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); track.get().modify()->setMediaLibrary(library.get()); } @@ -695,7 +696,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -859,7 +860,50 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Listen_getRecentArtists) + TEST_F(DatabaseFixture, Listen_getRecentReleaseArtists) + { + ScopedTrack track{ session }; + ScopedUser user{ session, "MyUser" }; + ScopedArtist artist{ session, "MyArtist" }; + ScopedRelease release{ session, "MyRelease" }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(release.get(), artist.get(), false); + } + + { + auto transaction{ session.createReadTransaction() }; + + Listen::ArtistStatsFindParameters params; + params.setUser(user->getId()); + params.setScrobblingBackend(ScrobblingBackend::Internal); + params.setReleaseArtistsOnly(true); + + auto artists{ Listen::getRecentArtists(session, params) }; + EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.moreResults, false); + } + + const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + + { + auto transaction{ session.createReadTransaction() }; + + Listen::ArtistStatsFindParameters params; + params.setUser(user->getId()); + params.setScrobblingBackend(ScrobblingBackend::Internal); + params.setReleaseArtistsOnly(true); + + auto artists{ Listen::getRecentArtists(session, params) }; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist->getId()); + } + } + + TEST_F(DatabaseFixture, Listen_getRecentTrackArtists) { ScopedTrack track{ session }; ScopedUser user{ session, "MyUser" }; @@ -867,7 +911,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -896,6 +940,20 @@ namespace lms::db::tests ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results[0], artist->getId()); } + + { + auto transaction{ session.createReadTransaction() }; + + Listen::ArtistStatsFindParameters params; + params.setUser(user->getId()); + params.setScrobblingBackend(ScrobblingBackend::Internal); + params.setReleaseArtistsOnly(true); + + auto artists{ Listen::getRecentArtists(session, params) }; + EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.moreResults, false); + } + { auto transaction{ session.createReadTransaction() }; @@ -912,7 +970,7 @@ namespace lms::db::tests Listen::ArtistStatsFindParameters params; params.setUser(user->getId()); params.setScrobblingBackend(ScrobblingBackend::Internal); - params.setLinkType(TrackArtistLinkType::Producer); + params.setTrackArtistLinkType(TrackArtistLinkType::Producer); auto artists{ Listen::getRecentArtists(session, params) }; EXPECT_EQ(artists.results.size(), 0); @@ -935,7 +993,7 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) + TEST_F(DatabaseFixture, Listen_getRecentTrackArtists_multi) { ScopedUser user{ session, "MyUser" }; ScopedTrack track1{ session }; @@ -946,8 +1004,8 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + session.create(track1.get(), artist1.get(), TrackArtistLinkType::Artist); + session.create(track2.get(), artist2.get(), TrackArtistLinkType::Artist); } { @@ -1014,7 +1072,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); } { @@ -1056,7 +1114,7 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); track.get().modify()->setMediaLibrary(library.get()); } diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 2d62e054..522e9097 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.cpp @@ -34,6 +34,7 @@ #include "database/objects/RatedArtist.hpp" #include "database/objects/RatedRelease.hpp" #include "database/objects/RatedTrack.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ScanSettings.hpp" #include "database/objects/StarredArtist.hpp" #include "database/objects/StarredRelease.hpp" @@ -368,6 +369,7 @@ VALUES EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{})); EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{})); EXPECT_FALSE(Release::find(session, ReleaseId{})); + EXPECT_FALSE(ReleaseArtistLink::find(session, ReleaseArtistLinkId{})); EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{})); EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{})); EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{})); diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 940f7b73..f81332b4 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -23,6 +23,8 @@ #include "database/objects/Artwork.hpp" #include "database/objects/Image.hpp" #include "database/objects/Medium.hpp" +#include "database/objects/ReleaseArtistLink.hpp" +#include "database/objects/Types.hpp" namespace lms::db::tests { @@ -689,7 +691,7 @@ namespace lms::db::tests } } - TEST_F(DatabaseFixture, Release_artist) + TEST_F(DatabaseFixture, Release_trackArtist) { ScopedRelease release{ session, "MyRelease" }; ScopedTrack track{ session }; @@ -703,21 +705,21 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; EXPECT_EQ(releases.results.size(), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })); EXPECT_EQ(releases.results.size(), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId())), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0); } { auto transaction{ session.createWriteTransaction() }; - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Producer); + session.create(track.get(), artist.get(), TrackArtistLinkType::Artist); + session.create(track.get(), artist.get(), TrackArtistLinkType::Producer); } { @@ -725,83 +727,124 @@ namespace lms::db::tests EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); - auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; + auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })); EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })); EXPECT_EQ(releases.results.size(), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId())); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId())); EXPECT_EQ(releases.results.size(), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Writer, TrackArtistLinkType::Artist })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 1); + EXPECT_EQ(release->getTrackArtists().size(), 1); + EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Artist).size(), 1); + EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer })); EXPECT_EQ(releases.results.size(), 0); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer })); - EXPECT_EQ(releases.results.size(), 0); - - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist })); - EXPECT_EQ(releases.results.size(), 0); - - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist, TrackArtistLinkType::Composer })); + releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer })); EXPECT_EQ(releases.results.size(), 0); } - } - - TEST_F(DatabaseFixture, Release_releaseArtist) - { - ScopedRelease release{ session, "MyRelease" }; - ScopedTrack track{ session }; - ScopedArtist artist{ session, "MyArtist" }; { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })) }; + { + bool visited{}; + release->visitTrackArtistLinks(TrackArtistLinkType::Artist, [&](const db::TrackArtistLink::pointer& link) { + visited = true; + EXPECT_EQ(link->getArtistId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } + + { + bool visited{}; + release->visitTrackArtistLinks(TrackArtistLinkType::Conductor, [&](const db::TrackArtistLink::pointer&) { + visited = true; + }); + EXPECT_FALSE(visited); + } + } + } + + TEST_F(DatabaseFixture, Release_artist) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedArtist artist1{ session, "MyArtist" }; + ScopedArtist artist2{ session, "MyArtist" }; + ScopedArtist artist3{ session, "MyArtist" }; + + { + auto transaction{ session.createReadTransaction() }; + + const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) }; EXPECT_EQ(releases.results.size(), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })), 0); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 0); - EXPECT_EQ(release->getArtists(TrackArtistLinkType::ReleaseArtist).size(), 0); - EXPECT_EQ(release->getArtistIds(TrackArtistLinkType::ReleaseArtist).size(), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 0); + EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0); + EXPECT_EQ(release->getArtistLinks().size(), 0); + + bool visited{}; + release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer&) { + visited = true; + }); + EXPECT_FALSE(visited); } { auto transaction{ session.createWriteTransaction() }; - track.get().modify()->setRelease(release.get()); - TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); + auto link1{ session.create(release.get(), artist1.get(), false) }; + auto link2{ session.create(release.get(), artist2.get(), true) }; + link2.modify()->setArtistName("MyArtistAlternateName"); + link2.modify()->setArtistSortName("MyArtistAlternateSortName"); } { auto transaction{ session.createReadTransaction() }; - auto artists{ release->getArtists(TrackArtistLinkType::ReleaseArtist) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + const auto links{ release->getArtistLinks() }; + ASSERT_EQ(links.size(), 2); + EXPECT_EQ(links[0]->getArtistId(), artist1.getId()); + EXPECT_EQ(links[0]->getArtistName(), ""); + EXPECT_EQ(links[0]->getArtistSortName(), ""); + EXPECT_EQ(links[1]->getArtistId(), artist2.getId()); + EXPECT_EQ(links[1]->getArtistName(), "MyArtistAlternateName"); + EXPECT_EQ(links[1]->getArtistSortName(), "MyArtistAlternateSortName"); + EXPECT_EQ(links[1]->isArtistMBIDMatched(), true); } { auto transaction{ session.createReadTransaction() }; - auto artists{ release->getArtistIds(TrackArtistLinkType::ReleaseArtist) }; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front(), artist.getId()); + std::vector links; + release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer& artistLink) { + links.push_back(artistLink); + }); + ASSERT_EQ(links.size(), 2); + EXPECT_EQ(links[0]->getArtistId(), artist1.getId()); + EXPECT_EQ(links[0]->getArtistName(), ""); + EXPECT_EQ(links[0]->getArtistSortName(), ""); + EXPECT_EQ(links[1]->getArtistId(), artist2.getId()); + EXPECT_EQ(links[1]->getArtistName(), "MyArtistAlternateName"); + EXPECT_EQ(links[1]->getArtistSortName(), "MyArtistAlternateSortName"); + EXPECT_EQ(links[1]->isArtistMBIDMatched(), true); } { @@ -809,13 +852,31 @@ namespace lms::db::tests EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); - const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })) }; + const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })), 1); - EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist1.getId())), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 1); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0); + EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId())), 1); + } + + { + auto transaction{ session.createReadTransaction() }; + + const auto artists{ release->getArtists() }; + EXPECT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist2.getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + EXPECT_TRUE(release->hasArtist(artist1.getId())); + EXPECT_FALSE(release->hasArtist(artist3.getId())); } } + TEST_F(DatabaseFixture, Release_isCompilation) { ScopedRelease release{ session, "MyRelease" }; diff --git a/src/libs/database/test/ReleaseArtistLink.cpp b/src/libs/database/test/ReleaseArtistLink.cpp new file mode 100644 index 00000000..6e6948e5 --- /dev/null +++ b/src/libs/database/test/ReleaseArtistLink.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 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 "Common.hpp" + +#include "database/objects/ReleaseArtistLink.hpp" + +namespace lms::db::tests +{ + TEST_F(DatabaseFixture, ReleaseArtistLink) + { + ScopedRelease release{ session, "MyRelease" }; + ScopedTrack track{ session }; + ScopedArtist artist{ session, "MyArtist" }; + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseArtistLink::FindParameters params; + params.setRelease(release.getId()); + + bool visited{}; + ReleaseArtistLink::find(session, params, [&](const ReleaseArtistLink::pointer&) { + visited = true; + }); + EXPECT_FALSE(visited); + } + + { + auto transaction{ session.createWriteTransaction() }; + + track.get().modify()->setRelease(release.get()); + session.create(release.get(), artist.get(), false); + } + + { + auto transaction{ session.createReadTransaction() }; + + ReleaseArtistLink::FindParameters params; + params.setRelease(release.getId()); + params.setMBIDMatched(false); + + bool visited{}; + ReleaseArtistLink::find(session, params, [&](const ReleaseArtistLink::pointer& link) { + visited = true; + EXPECT_EQ(link->getArtistId(), artist.getId()); + }); + EXPECT_TRUE(visited); + } + } +} // namespace lms::db::tests \ No newline at end of file diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp index c7e53313..1645626f 100644 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -100,7 +100,7 @@ namespace lms::feedback searchParams.setFilters(params.filters); searchParams.setStarringUser(params.user, *backend); searchParams.setKeywords(params.keywords); - searchParams.setLinkType(params.linkType); + searchParams.setTrackArtistLinkType(params.trackArtistLinkType); searchParams.setSortMethod(params.sortMethod); searchParams.setRange(params.range); diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index f54858e2..519bbcad 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -81,12 +81,18 @@ namespace lms::feedback // Artists struct ArtistFindParameters : public FindParameters { - std::optional linkType; // if set, only artists that have produced at least one track with this link type + std::optional trackArtistLinkType; // if set, only artists that have produced at least one track with this link type db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None }; + std::optional releaseArtistsOnly; - ArtistFindParameters& setLinkType(std::optional _linkType) + ArtistFindParameters& setReleaseArtistsOnly(std::optional _releaseArtistsOnly) { - linkType = _linkType; + releaseArtistsOnly = _releaseArtistsOnly; + return *this; + } + ArtistFindParameters& setTrackArtistLinkType(std::optional _trackArtistLinkType) + { + trackArtistLinkType = _trackArtistLinkType; return *this; } ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod) diff --git a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp index 51e94eb1..ac0d12c4 100644 --- a/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp +++ b/src/libs/services/scanner/impl/scanners/audiofile/AudioFileScanOperation.cpp @@ -38,6 +38,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackEmbeddedImage.hpp" @@ -80,6 +81,20 @@ namespace lms::scanner createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback); } + void createReleaseArtistLinks(db::Session& session, const db::Release::pointer& dbRelease, std::span artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback) + { + for (const Artist& artist : artists) + { + db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) }; + + const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid }; + db::ReleaseArtistLink::pointer link{ session.create(dbRelease, dbArtist, matchedUsingMbid) }; + link.modify()->setArtistName(artist.name); + if (artist.sortName) + link.modify()->setArtistSortName(*artist.sortName); + } + } + db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name) { db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) }; @@ -107,7 +122,46 @@ namespace lms::scanner return label; } - void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release) + bool needUpdateReleaseArtists(const db::Release::pointer& dbRelease, const Release& release) + { + const std::vector dbArtistLinks{ dbRelease->getArtistLinks() }; + + if (dbArtistLinks.size() != release.artists.size()) + return true; + + // Must be in same order + for (std::size_t i{}; i < dbArtistLinks.size(); ++i) + { + const db::ReleaseArtistLink::pointer& dbArtistLink{ dbArtistLinks[i] }; + const Artist& artist{ release.artists[i] }; + + if (dbArtistLink->getArtistName() != artist.name) + return true; + + if (dbArtistLink->getArtistSortName() != artist.sortName) + return true; + + if (!dbArtistLink->isArtistMBIDMatched() && artist.mbid) + return true; + + if (dbArtistLink->isArtistMBIDMatched() && !artist.mbid) + return true; + + if (artist.mbid) + { + const db::Artist::pointer dbArtist{ dbArtistLink->getArtist() }; + if (!dbArtist) + return true; + + if (dbArtistLink->getArtist()->getMBID() != artist.mbid) + return true; + } + } + + return false; + } + + void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release, helpers::AllowFallbackOnMBIDEntry allowFallback) { if (dbRelease->getName() != release.name) dbRelease.modify()->setName(release.name); @@ -143,6 +197,11 @@ namespace lms::scanner for (std::string_view label : release.labels) dbRelease.modify()->addLabel(getOrCreateLabel(session, label)); } + if (needUpdateReleaseArtists(dbRelease, release)) + { + dbRelease.modify()->clearArtistLinks(); + createReleaseArtistLinks(session, dbRelease, release.artists, allowFallback); + } } // Compare release level info @@ -157,7 +216,7 @@ namespace lms::scanner && dbCandidateRelease->getBarcode() == release.barcode; } - db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory) + db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory, helpers::AllowFallbackOnMBIDEntry allowFallback) { db::Release::pointer dbRelease; @@ -224,7 +283,7 @@ namespace lms::scanner if (!dbRelease) dbRelease = session.create(release.name); - updateReleaseIfNeeded(session, dbRelease, release); + updateReleaseIfNeeded(session, dbRelease, release, allowFallback); return dbRelease; } @@ -671,13 +730,24 @@ namespace lms::scanner db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary) }; track.modify()->setDirectory(directory); - track.modify()->clearArtistLinks(); - const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback); - if (_file->track.medium && _file->track.medium->release) - createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback); + // For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set + if (_file->track.medium && _file->track.medium->release) + { + db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory, allowFallback) }; + assert(release); + track.modify()->setRelease(release); + track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release)); + } + else + { + track.modify()->setRelease({}); + track.modify()->setMedium({}); + } + + track.modify()->clearArtistLinks(); + createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback); @@ -688,19 +758,6 @@ namespace lms::scanner for (const auto& [role, performers] : _file->track.performerArtists) createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); - // For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set - if (_file->track.medium && _file->track.medium->release) - { - db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) }; - assert(release); - track.modify()->setRelease(release); - track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release)); - } - else - { - track.modify()->setRelease({}); - track.modify()->setMedium({}); - } track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track)); track.modify()->setName(title); track.modify()->setTrackNumber(_file->track.position); diff --git a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp index 3b095f18..2a8ff7c7 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepArtistReconciliation.cpp @@ -28,6 +28,7 @@ #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/ArtistInfo.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackList.hpp" @@ -75,23 +76,46 @@ namespace lms::scanner artistInfo.modify()->setArtist(newArtist); } - db::TrackArtistLink::pointer getMostRecentMBIDArtistLink(db::Session& session, db::ArtistId artistId, std::optional linkType = std::nullopt) + struct ArtistReference { + std::string name; + std::string sortName; + }; + std::optional getMostRecentReleaseArtistReference(db::Session& session, db::ArtistId artistId) + { + std::optional ref; + + db::ReleaseArtistLink::FindParameters params; + params.setArtist(artistId); + params.setSortMethod(db::ReleaseArtistLinkSortMethod::OriginalDateDesc); + params.setMBIDMatched(true); + params.setRange(db::Range{ .offset = 0, .size = 1 }); + + db::ReleaseArtistLink::pointer foundLink; + db::ReleaseArtistLink::find(session, params, [&](const db::ReleaseArtistLink::pointer& link) { + ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } }; + }); + + return ref; + } + + std::optional getMostRecentTrackArtistReference(db::Session& session, db::ArtistId artistId) + { + std::optional ref; + db::TrackArtistLink::FindParameters params; params.setArtist(artistId); - params.setLinkType(linkType); params.setSortMethod(db::TrackArtistLinkSortMethod::OriginalDateDesc); params.setMBIDMatched(true); params.setRange(db::Range{ .offset = 0, .size = 1 }); db::TrackArtistLink::pointer foundLink; db::TrackArtistLink::find(session, params, [&](const db::TrackArtistLink::pointer& link) { - foundLink = link; + ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } }; }); - return foundLink; + return ref; } - } // namespace bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const @@ -173,24 +197,24 @@ namespace lms::scanner if (hasArtistInfo) continue; - db::TrackArtistLink::pointer artistMostRecentLink{ getMostRecentMBIDArtistLink(session, artist->getId(), db::TrackArtistLinkType::ReleaseArtist) }; - if (!artistMostRecentLink) - artistMostRecentLink = getMostRecentMBIDArtistLink(session, artist->getId()); + std::optional mostRecentArtistRef{ getMostRecentReleaseArtistReference(session, artist->getId()) }; + if (!mostRecentArtistRef) + mostRecentArtistRef = getMostRecentTrackArtistReference(session, artist->getId()); - if (!artistMostRecentLink) + if (!mostRecentArtistRef) { LMS_LOG(DBUPDATER, DEBUG, "Unable to fix name discrepancy for artist " << artist << ": no link found!"); continue; } - if (artistMostRecentLink->getArtistName() != artist->getName()) + if (mostRecentArtistRef->name != artist->getName()) { ArtistToUpdate& artistToUpdate{ artistsToUpdate.emplace_back() }; artistToUpdate.artist = artist; - artistToUpdate.newName = artistMostRecentLink->getArtistName(); - artistToUpdate.newSortName = artistMostRecentLink->getArtistSortName(); + artistToUpdate.newName = mostRecentArtistRef->name; + artistToUpdate.newSortName = mostRecentArtistRef->sortName; - LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent '" << db::trackArtistLinkTypeToString(artistMostRecentLink->getType()) << "' link reference"); + LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent artist link reference"); } } diff --git a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp index 3650ed5c..4b21ff37 100644 --- a/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp +++ b/src/libs/services/scanner/impl/steps/ScanStepAssociateArtistImages.cpp @@ -126,7 +126,7 @@ namespace lms::scanner std::set releasePaths; db::Directory::FindParameters params; - params.setArtist(artistId, { db::TrackArtistLinkType::ReleaseArtist }); + params.setReleaseArtist(artistId); db::Directory::find(session, params, [&](const db::Directory::pointer& directory) { releasePaths.insert(directory->getAbsolutePath()); @@ -176,7 +176,7 @@ namespace lms::scanner db::Artwork::pointer artwork; db::Release::FindParameters params; - params.setArtist(artist->getId(), { db::TrackArtistLinkType::ReleaseArtist }); + params.setArtist(artist->getId()); params.setSortMethod(db::ReleaseSortMethod::OriginalDate); db::Release::find(session, params, [&](const db::Release::pointer& release) { diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index 5e97a9a7..2b9ae3eb 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -51,7 +51,8 @@ namespace lms::scrobbling db::Listen::ArtistStatsFindParameters convertToListenFindParameters(const ScrobblingService::ArtistFindParameters& params) { - return db::Listen::ArtistStatsFindParameters{ convertToListenFindParameters(static_cast(params)), params.linkType }; + db::Listen::ArtistStatsFindParameters listenFindParams{ convertToListenFindParameters(static_cast(params)), params.linkType, params.releaseArtistsOnly }; + return listenFindParams; } } // namespace diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index a1ba0590..6895593d 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -36,6 +36,7 @@ #include "database/objects/Listen.hpp" #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" +#include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" #include "services/scrobbling/Exception.hpp" @@ -46,6 +47,27 @@ namespace lms::scrobbling::listenBrainz { namespace { + struct Artist + { + std::string name; + std::optional mbid; + }; + std::vector getTrackArtists(const db::Track::pointer& track) + { + std::vector artists; + + for (const db::TrackArtistLink::pointer& trackArtistLink : track->getArtistLinks(db::TrackArtistLinkType::Artist)) + { + const auto trackArtist{ trackArtistLink->getArtist() }; + if (!trackArtist) + continue; + + artists.emplace_back(Artist{ std::string{ trackArtistLink->getArtistName() }, trackArtist->getMBID() }); + } + + return artists; + } + std::optional listenToJsonPayload(db::Session& session, const scrobbling::Listen& listen, const Wt::WDateTime& timePoint) { auto transaction{ session.createReadTransaction() }; @@ -54,10 +76,7 @@ namespace lms::scrobbling::listenBrainz if (!track) return std::nullopt; - auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) }; - if (artists.empty()) - artists = track->getArtists({ db::TrackArtistLinkType::ReleaseArtist }); - + const std::vector artists{ getTrackArtists(track) }; if (artists.empty()) { LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist"); @@ -77,10 +96,10 @@ namespace lms::scrobbling::listenBrainz { Wt::Json::Array artistMBIDs; - for (const db::Artist::pointer& artist : artists) + for (const Artist& artist : artists) { - if (auto MBID{ artist->getMBID() }) - artistMBIDs.push_back(Wt::Json::Value{ std::string{ MBID->getAsString() } }); + if (artist.mbid) + artistMBIDs.push_back(Wt::Json::Value{ std::string{ artist.mbid->getAsString() } }); } if (!artistMBIDs.empty()) diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index ff5c016f..9d5559a2 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -97,8 +97,14 @@ namespace lms::scrobbling { std::optional linkType; // if set, only artists that have produced at least one track with this link type db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None }; + bool releaseArtistsOnly; - ArtistFindParameters& setLinkType(std::optional _linkType) + ArtistFindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly) + { + releaseArtistsOnly = _releaseArtistsOnly; + return *this; + } + ArtistFindParameters& setTrackArtistLinkType(std::optional _linkType) { linkType = _linkType; return *this; diff --git a/src/libs/subsonic/impl/endpoints/Browsing.cpp b/src/libs/subsonic/impl/endpoints/Browsing.cpp index 561e57c2..59d17a86 100644 --- a/src/libs/subsonic/impl/endpoints/Browsing.cpp +++ b/src/libs/subsonic/impl/endpoints/Browsing.cpp @@ -109,7 +109,7 @@ namespace lms::api::subsonic { // API says: "Returns a random collection of songs from the given artist and similar artists" const std::size_t similarArtistCount{ count / 5 }; - std::vector artistIds{ core::Service::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, similarArtistCount) }; + std::vector artistIds{ core::Service::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist }, similarArtistCount) }; artistIds.push_back(artistId); const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 }; @@ -392,16 +392,16 @@ namespace lms::api::subsonic case SubsonicArtistListMode::AllArtists: break; case SubsonicArtistListMode::ReleaseArtists: - parameters.setLinkType(TrackArtistLinkType::ReleaseArtist); + parameters.setReleaseArtistsOnly(true); break; case SubsonicArtistListMode::TrackArtists: - parameters.setLinkType(TrackArtistLinkType::Artist); + parameters.setTrackArtistLinkType(TrackArtistLinkType::Artist); break; } } parameters.filters.setMediaLibrary(mediaLibrary); - // This endpoint does not scale: make sort lived transactions in order not to block the whole application + // This endpoint does not scale: make short lived transactions in order not to block the whole application // first pass: dispatch the artists by first letter LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists..."); @@ -419,12 +419,7 @@ namespace lms::api::subsonic { std::string_view sortName{ artist->getSortName() }; - char sortChar; - if (sortName.empty() || !std::isalpha(sortName[0])) - sortChar = '#'; - else - sortChar = std::toupper(sortName[0]); - + const char sortChar{ (sortName.empty() || !std::isalpha(sortName[0])) ? '#' : static_cast(std::toupper(sortName[0])) }; artistsSortedByFirstChar[sortChar].push_back(artist->getId()); } @@ -465,9 +460,18 @@ namespace lms::api::subsonic Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response::Node artistNode{ createArtistNode(context, artist) }; - const auto releases{ Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; - for (const Release::pointer& release : releases.results) + auto addRelease{ [&](const Release::pointer& release) { artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */)); + } }; + + Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId()), [&](const db::Release::pointer& release) { + addRelease(release); + }); + + Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) { + if (!release->hasArtist(id)) + addRelease(release); + }); response.addNode("artist", std::move(artistNode)); @@ -561,7 +565,7 @@ namespace lms::api::subsonic }); } - auto similarArtistsId{ core::Service::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) }; + auto similarArtistsId{ core::Service::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist }, count) }; { auto transaction{ context.getDbSession().createReadTransaction() }; diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 61eaa8d1..56fdf1fd 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -29,6 +29,7 @@ #include "database/objects/Directory.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -99,23 +100,38 @@ namespace lms::api::subsonic else if (const auto year{ release->getYear() }) albumNode.setAttribute("year", *year); - auto artists{ release->getReleaseArtists() }; - if (artists.empty()) - artists = release->getArtists(); - - if (!artists.empty()) + struct Artist { - if (!release->getArtistDisplayName().empty()) - albumNode.setAttribute("artist", release->getArtistDisplayName()); - else - albumNode.setAttribute("artist", utils::joinArtistNames(artists)); + std::string name; + std::string id; + }; + std::optional artist; - if (artists.size() == 1) + const auto artistLinks{ release->getArtistLinks() }; + if (!artistLinks.empty()) + { + artist = Artist{ .name = std::string{ release->getArtistDisplayName() }, .id = {} }; + if (artistLinks.size() == 1) + artist->id = idToString(artistLinks.front()->getArtistId()); + } + else + { + if (const auto trackArtists{ release->getTrackArtists(TrackArtistLinkType::Artist) }; !trackArtists.empty()) { - albumNode.setAttribute("artistId", idToString(artists.front()->getId())); + if (trackArtists.size() > 1) + artist = Artist{ .name = "Various Artists", .id = {} }; + else if (trackArtists.size() == 1) + artist = Artist{ .name = trackArtists.front()->getName(), .id = idToString(trackArtists.front()->getId()) }; } } + if (artist) + { + albumNode.setAttribute("artist", artist->name); + if (!artist->id.empty()) + albumNode.setAttribute("artistId", artist->id); + } + albumNode.setAttribute("playCount", core::Service::get()->getCount(context.getUser()->getId(), release->getId())); // Report the first GENRE for this track @@ -182,16 +198,16 @@ namespace lms::api::subsonic if (id3) { albumNode.createEmptyArrayChild("artists"); - for (const Artist::pointer& artist : release->getReleaseArtists()) - albumNode.addArrayChild("artists", createArtistNode(artist)); + for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks) + albumNode.addArrayChild("artists", createMinimalArtistNode(artistLink)); albumNode.setAttribute("displayArtist", release->getArtistDisplayName()); } else { albumNode.createEmptyArrayChild("albumArtists"); - for (const Artist::pointer& artist : release->getReleaseArtists()) - albumNode.addArrayChild("albumArtists", createArtistNode(artist)); + for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks) + albumNode.addArrayChild("albumArtists", createMinimalArtistNode(artistLink)); albumNode.setAttribute("displayAlbumArtist", release->getArtistDisplayName()); diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index af4e88b4..a7642bbf 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -26,6 +26,7 @@ #include "database/objects/Artist.hpp" #include "database/objects/Artwork.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -66,18 +67,25 @@ namespace lms::api::subsonic { LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist"); - Response::Node artistNode{ createArtistNode(artist) }; + Response::Node artistNode{ createMinimalArtistNode(artist) }; - artistNode.setAttribute("id", idToString(artist->getId())); - artistNode.setAttribute("name", artist->getName()); if (const auto artwork{ artist->getPreferredArtwork() }) { CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; artistNode.setAttribute("coverArt", idToString(coverArtId)); } - const std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; - artistNode.setAttribute("albumCount", count); + { + std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; + + // TODO: not very efficient + Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) { + if (!release->hasArtist(artist->getId())) + count++; + }); + + artistNode.setAttribute("albumCount", count); + } if (const Wt::WDateTime dateTime{ core::Service::get()->getStarredDateTime(context.getUser()->getId(), artist->getId()) }; dateTime.isValid()) artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); @@ -107,7 +115,7 @@ namespace lms::api::subsonic return artistNode; } - Response::Node createArtistNode(const Artist::pointer& artist) + Response::Node createMinimalArtistNode(const Artist::pointer& artist) { Response::Node artistNode; @@ -116,4 +124,24 @@ namespace lms::api::subsonic return artistNode; } + + Response::Node createMinimalArtistNode(const db::ObjectPtr& artistLink) + { + Response::Node artistNode; + + artistNode.setAttribute("id", idToString(artistLink->getArtistId())); + artistNode.setAttribute("name", artistLink->getArtistName()); + + return artistNode; + } + + Response::Node createMinimalArtistNode(const db::ObjectPtr& artistLink) + { + Response::Node artistNode; + + artistNode.setAttribute("id", idToString(artistLink->getArtistId())); + artistNode.setAttribute("name", artistLink->getArtistName()); + + return artistNode; + } } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Artist.hpp b/src/libs/subsonic/impl/responses/Artist.hpp index 1ae4ec86..50612d76 100644 --- a/src/libs/subsonic/impl/responses/Artist.hpp +++ b/src/libs/subsonic/impl/responses/Artist.hpp @@ -30,6 +30,8 @@ namespace lms::db { class Artist; + class ReleaseArtistLink; + class TrackArtistLink; class User; class Session; } // namespace lms::db @@ -45,5 +47,7 @@ namespace lms::api::subsonic } // namespace utils Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr& artist); - Response::Node createArtistNode(const db::ObjectPtr& artist); // only minimal info + Response::Node createMinimalArtistNode(const db::ObjectPtr& artist); // only minimal info + Response::Node createMinimalArtistNode(const db::ObjectPtr& artistLink); // only minimal info + Response::Node createMinimalArtistNode(const db::ObjectPtr& artistLink); // only minimal info } // namespace lms::api::subsonic \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Contributor.cpp b/src/libs/subsonic/impl/responses/Contributor.cpp index 430f5d5a..b36e9f38 100644 --- a/src/libs/subsonic/impl/responses/Contributor.cpp +++ b/src/libs/subsonic/impl/responses/Contributor.cpp @@ -27,14 +27,14 @@ namespace lms::api::subsonic { - Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink, const db::ObjectPtr& artist) + Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink) { Response::Node contributorNode; contributorNode.setAttribute("role", utils::toString(trackArtistLink->getType())); if (!trackArtistLink->getSubType().empty()) contributorNode.setAttribute("subRole", trackArtistLink->getSubType()); - contributorNode.addChild("artist", createArtistNode(artist)); + contributorNode.addChild("artist", createMinimalArtistNode(trackArtistLink)); return contributorNode; } diff --git a/src/libs/subsonic/impl/responses/Contributor.hpp b/src/libs/subsonic/impl/responses/Contributor.hpp index a563d7fe..677e01e3 100644 --- a/src/libs/subsonic/impl/responses/Contributor.hpp +++ b/src/libs/subsonic/impl/responses/Contributor.hpp @@ -31,5 +31,5 @@ namespace lms::db namespace lms::api::subsonic { - Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink, const db::ObjectPtr& artist); + Response::Node createContributorNode(const db::ObjectPtr& trackArtistLink); } \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index eb347a76..e7778066 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -27,6 +27,7 @@ #include "core/MimeTypes.hpp" #include "core/Service.hpp" #include "core/String.hpp" + #include "database/Types.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Artwork.hpp" @@ -35,6 +36,7 @@ #include "database/objects/MediaLibrary.hpp" #include "database/objects/Medium.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" @@ -198,19 +200,23 @@ namespace lms::api::subsonic trackResponse.createEmptyArrayChild("artists"); trackResponse.createEmptyArrayChild("contributors"); - db::TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const db::TrackArtistLink::pointer& link, const db::Artist::pointer& artist) { - switch (link->getType()) + track->visitArtistLinks([&](const db::TrackArtistLink::pointer& artistLink) { + switch (artistLink->getType()) { case db::TrackArtistLinkType::Artist: - trackResponse.addArrayChild("artists", createArtistNode(artist)); - break; - case db::TrackArtistLinkType::ReleaseArtist: - trackResponse.addArrayChild("albumArtists", createArtistNode(artist)); + trackResponse.addArrayChild("artists", createMinimalArtistNode(artistLink)); break; default: - trackResponse.addArrayChild("contributors", createContributorNode(link, artist)); + trackResponse.addArrayChild("contributors", createContributorNode(artistLink)); } }); + + if (release) + { + release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer& artistLink) { + trackResponse.addArrayChild("albumArtists", createMinimalArtistNode(artistLink)); + }); + } } trackResponse.setAttribute("displayArtist", track->getArtistDisplayName()); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 98d06c54..4c44ae62 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(lms ui/common/UUIDValidator.cpp ui/explore/ArtistCollector.cpp ui/explore/ArtistListHelpers.cpp + ui/explore/ArtistType.cpp ui/explore/ArtistView.cpp ui/explore/ArtistsView.cpp ui/explore/DatabaseCollectorBase.cpp diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index 6155327d..1cbd635c 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -35,6 +35,7 @@ #include "database/objects/Release.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackList.hpp" +#include "database/objects/Types.hpp" #include "database/objects/User.hpp" #include "LmsApplication.hpp" @@ -194,7 +195,7 @@ namespace lms::ui _audioFileResource = std::make_unique(); _title = bindNew("title"); - _artist = bindNew("artist"); + _artists = bindNew("artist"); _release = bindNew("release"); _separator = bindNew("separator"); _playQueue = bindNew("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML); @@ -237,10 +238,12 @@ namespace lms::ui if (!track) return; + const auto release{ track->getRelease() }; + const std::string transcodingResource{ _audioTranscodingResource->getUrl(trackId) }; const std::string nativeResource{ _audioFileResource->getUrl(trackId) }; - const auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) }; + const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) }; oss << "var params = {" @@ -250,8 +253,8 @@ namespace lms::ui << " duration: " << std::chrono::duration_cast>(track->getDuration()).count() << "," << " replayGain: " << replayGain << "," << " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\"," - << " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," - << " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","; + << " artist: \"" << (!artistDisplayInfo.displayName.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," + << " release: \"" << (release ? core::stringUtils::jsEscape(release->getName()) : "") << "\","; db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; if (!artworkId.isValid()) @@ -281,22 +284,12 @@ namespace lms::ui _title->setTextFormat(Wt::TextFormat::Plain); _title->setText(Wt::WString::fromUTF8(track->getName())); - bool needSeparator{ true }; + _artists->clear(); + _artists->addWidget(utils::createArtistsAnchors(artistDisplayInfo)); - if (!artists.empty()) - { - _artist->setTextFormat(Wt::TextFormat::Plain); - _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); - _artist->setLink(utils::createArtistLink(artists.front())); - } - else - { - _artist->setText(""); - _artist->setLink({}); - needSeparator = false; - } + bool needSeparator{ !artistDisplayInfo.displayName.empty() }; - if (const db::Release::pointer release{ track->getRelease() }) + if (release) { _release->setTextFormat(Wt::TextFormat::Plain); _release->setText(Wt::WString::fromUTF8(std::string{ release->getName() })); diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 7620b6fe..f25ffd1d 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -126,7 +127,7 @@ namespace lms::ui Wt::WText* _title{}; Wt::WAnchor* _release{}; Wt::WText* _separator{}; - Wt::WAnchor* _artist{}; + Wt::WContainerWidget* _artists{}; Wt::WPushButton* _playQueue{}; }; } // namespace lms::ui diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index b9f7b7e4..e4c214b5 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -487,8 +487,8 @@ namespace lms::ui if (!artists.empty()) { entry->setCondition("if-has-artists", true); - entry->bindWidget("artists", utils::createArtistAnchorList(artists)); - entry->bindWidget("artists-md", utils::createArtistAnchorList(artists)); + entry->bindWidget("artists", utils::createArtistsAnchors(track, db::TrackArtistLinkType::Artist)); + entry->bindWidget("artists-md", utils::createArtistsAnchors(track, db::TrackArtistLinkType::Artist)); } db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 8707d4a7..36fe516b 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -457,13 +457,13 @@ namespace lms::ui _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name); _artistRelationshipsModel = std::make_shared(); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-composer", 2), db::TrackArtistLinkType::Composer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-conductor", 2), db::TrackArtistLinkType::Conductor); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-lyricist", 2), db::TrackArtistLinkType::Lyricist); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-mixer", 2), db::TrackArtistLinkType::Mixer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-performer", 2), db::TrackArtistLinkType::Performer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-producer", 2), db::TrackArtistLinkType::Producer); - _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-remixer", 2), db::TrackArtistLinkType::Remixer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer); + _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer); _transcodingModeModeModel = std::make_shared(); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); diff --git a/src/lms/ui/State.hpp b/src/lms/ui/State.hpp index 8f51338c..3ee4fcf0 100644 --- a/src/lms/ui/State.hpp +++ b/src/lms/ui/State.hpp @@ -35,7 +35,7 @@ namespace lms::ui::state } // namespace detail template - void writeValue(std::string_view item, std::optional value) + void writeValue(std::string_view item, const std::optional& value) { if (value.has_value()) { diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index b9239eba..9149c0f0 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -19,6 +19,7 @@ #include "Utils.hpp" +#include #include #include @@ -30,8 +31,10 @@ #include "database/objects/Cluster.hpp" #include "database/objects/Image.hpp" #include "database/objects/Release.hpp" +#include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/ScanSettings.hpp" #include "database/objects/Track.hpp" +#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackList.hpp" #include "LmsApplication.hpp" @@ -51,6 +54,28 @@ namespace lms::ui::utils return image; } + + Wt::WLink createArtistLink(db::Artist::pointer artist) + { + if (const auto mbid{ artist->getMBID() }) + return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string{ mbid->getAsString() } }; + else + return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() }; + } + + std::unique_ptr createArtistAnchor(db::Artist::pointer artist, std::string_view displayName, bool setText) + { + auto res{ std::make_unique(createArtistLink(artist)) }; + + if (setText) + { + res->setTextFormat(Wt::TextFormat::Plain); + res->setText(Wt::WString::fromUTF8(std::string{ displayName })); + res->setToolTip(Wt::WString::fromUTF8(std::string{ displayName }), Wt::TextFormat::Plain); + } + + return res; + } } // namespace std::string durationToString(std::chrono::milliseconds msDuration) @@ -174,138 +199,38 @@ namespace lms::ui::utils return clusterContainer; } - std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass) + std::unique_ptr createArtistAnchorList(const std::vector& artists, std::string_view cssAnchorClass) { - using namespace db; - std::unique_ptr artistContainer{ std::make_unique() }; bool firstArtist{ true }; - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - for (const ArtistId artistId : artistIds) + for (const auto& artist : artists) { - const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) }; - if (!artist) - continue; - if (!firstArtist) artistContainer->addNew(" · "); + firstArtist = false; auto anchor{ createArtistAnchor(artist) }; anchor->addStyleClass("text-decoration-none"); // hack anchor->addStyleClass(std::string{ cssAnchorClass }); artistContainer->addWidget(std::move(anchor)); - firstArtist = false; } return artistContainer; } - std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass) - { - using namespace db; - - std::size_t matchCount{}; - std::string_view::size_type currentOffset{}; - - auto result{ std::make_unique() }; - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - // consider order is guaranteed + we will likely succeed - for (const ArtistId artistId : artistIds) - { - const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) }; - if (!artist) - break; - - const auto pos{ displayName.find(artist->getName(), currentOffset) }; - if (pos == std::string_view::npos) - break; - - assert(pos >= currentOffset); - if (pos != currentOffset) - result->addNew(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain); - - auto anchor{ createArtistAnchor(artist) }; - anchor->addStyleClass("text-decoration-none"); // hack - anchor->addStyleClass(std::string{ cssAnchorClass }); // hack - result->addWidget(std::move(anchor)); - currentOffset = pos + artist->getName().size(); - matchCount += 1; - } - - if (matchCount == artistIds.size()) - { - const std::string_view remainingStr{ displayName.substr(currentOffset) }; - if (!remainingStr.empty()) - result->addNew(std::string{ remainingStr }, Wt::TextFormat::Plain); - } - else - result = createArtistAnchorList(artistIds, cssAnchorClass); - - return result; - } - - std::unique_ptr createArtistsAnchorsForRelease(db::ObjectPtr release, db::ArtistId omitIfMatchThisArtist, std::string_view cssAnchorClass) - { - using namespace db; - - if (const std::vector releaseArtists{ release->getArtistIds(TrackArtistLinkType::ReleaseArtist) }; !releaseArtists.empty()) - { - if (releaseArtists.size() == 1 && releaseArtists.front() == omitIfMatchThisArtist) - return {}; - - return createArtistDisplayNameWithAnchors(release->getArtistDisplayName(), releaseArtists, cssAnchorClass); - } - - const auto artists{ release->getArtistIds(TrackArtistLinkType::Artist) }; - if (artists.size() == 1) - { - if (artists.front() == omitIfMatchThisArtist) - return {}; - - return createArtistAnchorList({ artists.front() }, cssAnchorClass); - } - - if (artists.size() > 1) - { - auto res{ std::make_unique() }; - res->addNew(Wt::WString::tr("Lms.Explore.various-artists")); - return res; - } - - return {}; - } - - Wt::WLink createArtistLink(db::Artist::pointer artist) - { - if (const auto mbid{ artist->getMBID() }) - return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/mbid/" + std::string{ mbid->getAsString() } }; - else - return Wt::WLink{ Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString() }; - } - std::unique_ptr createArtistAnchor(db::Artist::pointer artist, bool setText) { - auto res = std::make_unique(createArtistLink(artist)); - - if (setText) - { - res->setTextFormat(Wt::TextFormat::Plain); - res->setText(Wt::WString::fromUTF8(artist->getName())); - res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); - } - - return res; + return createArtistAnchor(artist, artist->getName(), setText); } Wt::WLink createReleaseLink(db::Release::pointer release) { if (const auto mbid{ release->getMBID() }) return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string{ mbid->getAsString() } }; - else - return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() }; + + return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() }; } std::unique_ptr createReleaseAnchor(db::Release::pointer release, bool setText) @@ -344,4 +269,209 @@ namespace lms::ui::utils return res; } + + ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr release) + { + ArtistDisplayInfo res; + + res.displayName = release->getArtistDisplayName(); + release->visitArtistLinks([&res](const db::ObjectPtr& artistLink) { + res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ artistLink->getArtistName() }, .artist = artistLink->getArtist() }); + }); + + // If no release artists, fallback on track artists, only if there are only 1 artist, otherwise just put various-artists + if (res.entries.empty()) + { + res.displayName.clear(); + + const auto trackArtists{ release->getTrackArtists(db::TrackArtistLinkType::Artist) }; + + if (trackArtists.size() > 1) + res.displayName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8(); + else + res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ trackArtists[0]->getName() }, .artist = trackArtists[0] }); + } + + return res; + } + + ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr track, db::TrackArtistLinkType linkType) + { + ArtistDisplayInfo res; + + res.displayName = track->getArtistDisplayName(); + track->visitArtistLinks(linkType, [&res](const db::ObjectPtr& artistLink) { + res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ artistLink->getArtistName() }, .artist = artistLink->getArtist() }); + }); + + return res; + } + + std::unique_ptr createArtistsAnchors(const ArtistDisplayInfo& artistDisplayInfo, std::string_view cssAnchorClass) + { + auto result{ std::make_unique() }; + result->setInline(true); // TODO: use a template for that? + + std::size_t matchCount{}; + std::string_view::size_type currentOffset{}; + + if (artistDisplayInfo.entries.empty()) + { + if (!artistDisplayInfo.displayName.empty()) + result->addNew(std::string{ artistDisplayInfo.displayName }, Wt::TextFormat::Plain); + + return result; + } + + // consider order is guaranteed + we will likely succeed + for (const ArtistDisplayInfo::Entry& entry : artistDisplayInfo.entries) + { + const auto pos{ artistDisplayInfo.displayName.find(entry.displayName, currentOffset) }; + if (pos == std::string_view::npos) + break; + + assert(pos >= currentOffset); + if (pos != currentOffset) + result->addNew(std::string{ artistDisplayInfo.displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain); + + auto anchor{ createArtistAnchor(entry.artist, entry.displayName, true) }; + anchor->addStyleClass("text-decoration-none"); // hack + anchor->addStyleClass(std::string{ cssAnchorClass }); // hack + result->addWidget(std::move(anchor)); + + currentOffset = pos + entry.displayName.size(); + matchCount += 1; + } + + if (matchCount == artistDisplayInfo.entries.size()) + { + const std::string_view remainingStr{ artistDisplayInfo.displayName.substr(currentOffset) }; + if (!remainingStr.empty()) + result->addNew(std::string{ remainingStr }, Wt::TextFormat::Plain); + } + else + { + std::vector artists; + std::transform(std::cbegin(artistDisplayInfo.entries), std::cend(artistDisplayInfo.entries), std::back_inserter(artists), [](const ArtistDisplayInfo::Entry& entry) { + return entry.artist; + }); + + result = createArtistAnchorList(artists, cssAnchorClass); + } + + return result; + } + + std::unique_ptr createArtistsAnchors(db::ObjectPtr track, db::TrackArtistLinkType linkType, std::string_view cssAnchorClass) + { + const auto computeArtistDisplayInfo{ utils::computeArtistDisplayInfo(track, linkType) }; + return utils::createArtistsAnchors(computeArtistDisplayInfo, cssAnchorClass); + } + + std::unique_ptr createArtistsAnchors(db::ObjectPtr release, std::string_view cssAnchorClass) + { + const auto computeArtistDisplayInfo{ utils::computeArtistDisplayInfo(release) }; + return utils::createArtistsAnchors(computeArtistDisplayInfo, cssAnchorClass); + } + + std::map>> getArtistsByRole(db::ObjectPtr track, core::EnumSet artistLinkTypes) + { + std::map> artistMap; + + auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) { + if (!artistLinkTypes.empty() && !artistLinkTypes.contains(linkType)) + return; + + std::vector artists; + track->visitArtistLinks(linkType, [&](const auto& link) { + artists.push_back(link->getArtist()); + }); + + if (artists.empty()) + return; + + Wt::WString typeStr{ Wt::WString::trn(type, artists.size()) }; + artistMap[typeStr] = std::move(artists); + }; + + std::vector> rolelessPerformers; + auto addPerformerArtists = [&] { + if (!artistLinkTypes.empty() && !artistLinkTypes.contains(db::TrackArtistLinkType::Performer)) + return; + + track->visitArtistLinks(db::TrackArtistLinkType::Performer, [&](const auto& link) { + if (link->getSubType().empty()) + rolelessPerformers.push_back(link->getArtist()); + else + artistMap[std::string{ link->getSubType() }].push_back(link->getArtist()); + }); + }; + + addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer"); + addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor"); + addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist"); + addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer"); + addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer"); + addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer"); + addPerformerArtists(); + + if (!rolelessPerformers.empty()) + { + Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) }; + artistMap[performersStr] = std::move(rolelessPerformers); + } + + return artistMap; + } + + std::map>> getTrackArtistsByRole(db::ObjectPtr release) + { + std::map> artistMap; + + auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) { + std::vector artists; + release->visitTrackArtists(linkType, [&](const auto& artist) { + artists.push_back(artist); + }); + + if (artists.empty()) + return; + + Wt::WString typeStr{ Wt::WString::trn(type, artists.size()) }; + artistMap[typeStr] = std::move(artists); + }; + + std::vector> rolelessPerformers; + auto addPerformerArtists = [&] { + release->visitTrackArtistLinks(db::TrackArtistLinkType::Performer, [&](const db::TrackArtistLink::pointer& link) { + if (link->getSubType().empty()) + { + if (std::find(std::cbegin(rolelessPerformers), std::cend(rolelessPerformers), link->getArtist()) == std::cend(rolelessPerformers)) + rolelessPerformers.push_back(link->getArtist()); + } + else + { + auto& artists{ artistMap[std::string{ link->getSubType() }] }; + if (std::find(std::cbegin(artists), std::cend(artists), link->getArtist()) == std::cend(artists)) + artists.push_back(link->getArtist()); + } + }); + }; + + addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer"); + addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor"); + addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist"); + addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer"); + addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer"); + addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer"); + addPerformerArtists(); + + if (!rolelessPerformers.empty()) + { + Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) }; + artistMap[performersStr] = std::move(rolelessPerformers); + } + + return artistMap; + } } // namespace lms::ui::utils diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 0c849fde..0b083c41 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -20,21 +20,22 @@ #pragma once #include +#include #include #include +#include #include #include #include #include -#include "database/Object.hpp" -#include "database/objects/ArtistId.hpp" -#include "database/objects/ClusterId.hpp" -#include "database/objects/ReleaseId.hpp" -#include "database/objects/TrackId.hpp" +#include "core/EnumSet.hpp" + +#include "database/Object.hpp" +#include "database/objects/ClusterId.hpp" +#include "database/objects/Types.hpp" -#include "database/Types.hpp" #include "resource/ArtworkResource.hpp" namespace lms::db @@ -64,13 +65,30 @@ namespace lms::ui::utils std::unique_ptr createFilterCluster(db::ClusterId clusterId, bool canDelete = false); std::unique_ptr createFilterClustersForTrack(db::ObjectPtr track, Filters& filters); - std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); - std::unique_ptr createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector& artistIds, std::string_view cssAnchorClass = "link-success"); - std::unique_ptr createArtistsAnchorsForRelease(db::ObjectPtr release, db::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success"); - - Wt::WLink createArtistLink(db::ObjectPtr artist); std::unique_ptr createArtistAnchor(db::ObjectPtr artist, bool setText = true); Wt::WLink createReleaseLink(db::ObjectPtr release); std::unique_ptr createReleaseAnchor(db::ObjectPtr release, bool setText = true); std::unique_ptr createTrackListAnchor(db::ObjectPtr trackList, bool setText = true); + + std::unique_ptr createArtistAnchorList(const std::vector>& artists, std::string_view cssAnchorClass = "link-success"); + + struct ArtistDisplayInfo + { + std::string displayName; + struct Entry + { + std::string displayName; + db::ObjectPtr artist; + }; + std::vector entries; + }; + ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr release); + ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr track, db::TrackArtistLinkType linkType); + + std::unique_ptr createArtistsAnchors(const ArtistDisplayInfo& artistDisplayInfo, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistsAnchors(db::ObjectPtr track, db::TrackArtistLinkType linkType, std::string_view cssAnchorClass = "link-success"); + std::unique_ptr createArtistsAnchors(db::ObjectPtr release, std::string_view cssAnchorClass = "link-success"); + + std::map>> getArtistsByRole(db::ObjectPtr track, core::EnumSet artistLinkTypes = {}); + std::map>> getTrackArtistsByRole(db::ObjectPtr release); } // namespace lms::ui::utils diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 3d4f850d..03263039 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -20,14 +20,16 @@ #include "ArtistCollector.hpp" #include "core/Service.hpp" +#include "core/Utils.hpp" + #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/TrackList.hpp" +#include "database/objects/Types.hpp" #include "database/objects/User.hpp" #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "Filters.hpp" #include "LmsApplication.hpp" namespace lms::ui @@ -53,9 +55,14 @@ namespace lms::ui params.setFilters(getDbFilters()); params.setUser(LmsApp->getUserId()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setSortMethod(db::ArtistSortMethod::StarredDateDesc); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); + artists = feedbackService.findStarredArtists(params); break; } @@ -66,8 +73,12 @@ namespace lms::ui params.setUser(LmsApp->getUserId()); params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); artists = scrobblingService.getRecentArtists(params); break; @@ -79,8 +90,12 @@ namespace lms::ui params.setUser(LmsApp->getUserId()); params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); artists = scrobblingService.getTopArtists(params); break; @@ -91,9 +106,13 @@ namespace lms::ui db::Artist::FindParameters params; params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setSortMethod(db::ArtistSortMethod::AddedDesc); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -107,9 +126,13 @@ namespace lms::ui db::Artist::FindParameters params; params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setSortMethod(db::ArtistSortMethod::LastWrittenDesc); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -123,9 +146,13 @@ namespace lms::ui db::Artist::FindParameters params; params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setSortMethod(db::ArtistSortMethod::SortName); params.setRange(range); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -150,9 +177,13 @@ namespace lms::ui db::Artist::FindParameters params; params.setFilters(getDbFilters()); params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); params.setSortMethod(db::ArtistSortMethod::Random); params.setRange(db::Range{ 0, getMaxCount() }); + std::visit(core::utils::overloads{ + [&](AllArtistsTag) {}, + [&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); }, + [&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } }, + _artistType); { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index 6d96602b..6a61706e 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -22,8 +22,8 @@ #include #include "database/objects/ArtistId.hpp" -#include "database/objects/Types.hpp" +#include "ArtistType.hpp" #include "DatabaseCollectorBase.hpp" namespace lms::db @@ -40,11 +40,11 @@ namespace lms::ui db::RangeResults get(std::optional range = std::nullopt); void reset() { _randomArtists.reset(); } - void setArtistLinkType(std::optional linkType) { _linkType = linkType; } + void setArtistType(ArtistType artistType) { _artistType = artistType; } private: db::RangeResults getRandomArtists(Range range); std::optional> _randomArtists; - std::optional _linkType; + ArtistType _artistType; }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistType.cpp b/src/lms/ui/explore/ArtistType.cpp new file mode 100644 index 00000000..e36ba214 --- /dev/null +++ b/src/lms/ui/explore/ArtistType.cpp @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +#include "ArtistType.hpp" + +#include "core/Utils.hpp" + +namespace lms::core::stringUtils +{ + template<> + std::optional readAs(std::string_view str) + { + if (str == "all") + return lms::ui::AllArtistsTag{}; + if (str == "release") + return lms::ui::ReleaseArtistsTag{}; + + using UnderlyingType = std::underlying_type_t; + std::optional underlyingValue{ readAs(str) }; + if (!underlyingValue) + return std::nullopt; + + return lms::db::TrackArtistLinkType{ *underlyingValue }; + } +} // namespace lms::core::stringUtils + +namespace std +{ + std::string to_string(const lms::ui::ArtistType& artistType) + { + std::string res; + std::visit(lms::core::utils::overloads{ + [&](lms::ui::AllArtistsTag) { res = "all"; }, + [&](lms::ui::ReleaseArtistsTag) { res = "release"; }, + [&](lms::db::TrackArtistLinkType linkType) { res = std::to_string(static_cast>(linkType)); } }, + artistType); + + return res; + } +} // namespace std diff --git a/src/lms/ui/explore/ArtistType.hpp b/src/lms/ui/explore/ArtistType.hpp new file mode 100644 index 00000000..dcea6a91 --- /dev/null +++ b/src/lms/ui/explore/ArtistType.hpp @@ -0,0 +1,52 @@ +/* + * 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 "core/String.hpp" + +#include "database/objects/Types.hpp" + +namespace lms::ui +{ + struct AllArtistsTag + { + bool operator==(const AllArtistsTag&) const = default; + }; + struct ReleaseArtistsTag + { + bool operator==(const ReleaseArtistsTag&) const = default; + }; + + using ArtistType = std::variant; + +} // namespace lms::ui + +namespace lms::core::stringUtils +{ + template<> + [[nodiscard]] std::optional readAs(std::string_view str); +} + +namespace std +{ + [[nodiscard]] std::string to_string(const lms::ui::ArtistType& artistType); +}; \ No newline at end of file diff --git a/src/lms/ui/explore/ArtistTypeSelector.hpp b/src/lms/ui/explore/ArtistTypeSelector.hpp new file mode 100644 index 00000000..aaa56726 --- /dev/null +++ b/src/lms/ui/explore/ArtistTypeSelector.hpp @@ -0,0 +1,45 @@ +/* + * 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 "ArtistType.hpp" +#include "DropDownMenuSelector.hpp" + +namespace lms::ui +{ + class ArtistTypeSelector : public DropDownMenuSelector + { + public: + ArtistTypeSelector(ArtistType defaultArtistType) + : DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artists.template.artist-type-selector"), defaultArtistType } + { + bindItem("artist-type-all", Wt::WString::tr("Lms.Explore.Artists.all"), AllArtistsTag{}); + bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.Artists.release-artist", 2), ReleaseArtistsTag{}); + bindItem("artist-type-track-artist", Wt::WString::trn("Lms.Explore.Artists.track-artist", 2), db::TrackArtistLinkType::Artist); + bindItem("artist-type-composer", Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer); + bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor); + bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist); + bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer); + bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer); + bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer); + bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer); + } + }; +} // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 297f58eb..98096848 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -97,11 +97,12 @@ namespace lms::ui clear(); _artistId = {}; _trackContainer = nullptr; + _artistName = {}; if (!artistId) throw ArtistNotFoundException{}; - const auto similarArtistIds{ core::Service::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist, db::TrackArtistLinkType::ReleaseArtist }, 6) }; + const auto similarArtistIds{ core::Service::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist }, 6) }; auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -111,6 +112,7 @@ namespace lms::ui LmsApp->setTitle(artist->getName()); _artistId = *artistId; + _artistName = artist->getName(); refreshArtwork(artist->getPreferredArtworkId()); refreshArtistInfo(); @@ -139,7 +141,7 @@ namespace lms::ui } } - bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); + bindString("name", Wt::WString::fromUTF8(_artistName), Wt::TextFormat::Plain); bindNew("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) ->clicked() @@ -230,20 +232,24 @@ namespace lms::ui _releaseContainers.clear(); db::Release::FindParameters params; - params.setArtist(_artistId, { db::TrackArtistLinkType::ReleaseArtist }, {}); + params.setArtist(_artistId); params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod()); - const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) }; - if (!releases.results.empty()) - { - // first pass: gather all ids and sort by release type - for (const db::ReleaseId releaseId : releases.results) - { - const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) }; + // first pass: gather all ids and sort by release type + bool hasReleaseWithDifferentName{}; + db::Release::find(LmsApp->getDbSession(), params, [&](const db::Release::pointer& release) { + ReleaseType releaseType{ parseReleaseType(release->getReleaseTypeNames()) }; + _releaseContainers[releaseType].releases.push_back(release->getId()); - ReleaseType releaseType{ parseReleaseType(release->getReleaseTypeNames()) }; - _releaseContainers[releaseType].releases.push_back(releaseId); - } + if (release->getArtistDisplayName() != _artistName) + hasReleaseWithDifferentName = true; + }); + + if (!_releaseContainers.empty()) + { + core::EnumSet displayOptions{ releaseListHelpers::DisplayOptions::ShowYearAndOriginalYear }; + if (hasReleaseWithDifferentName) + displayOptions.insert(releaseListHelpers::DisplayOptions::ShowArtist); // second pass: construct widgets Wt::WContainerWidget* releaseContainers{ bindNew("release-containers") }; @@ -253,8 +259,8 @@ namespace lms::ui releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType)); releases.container = releaseContainer->bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); - releases.container->onRequestElements.connect(this, [this, &releases = releases] { - addSomeReleases(releases); + releases.container->onRequestElements.connect(this, [&, displayOptions] { + addSomeReleases(releases, displayOptions); }); } } @@ -266,35 +272,24 @@ namespace lms::ui void Artist::refreshAppearsOnReleases() { - constexpr core::EnumSet types{ - db::TrackArtistLinkType::Artist, - db::TrackArtistLinkType::Arranger, - db::TrackArtistLinkType::Composer, - db::TrackArtistLinkType::Conductor, - db::TrackArtistLinkType::Lyricist, - db::TrackArtistLinkType::Mixer, - db::TrackArtistLinkType::Performer, - db::TrackArtistLinkType::Producer, - db::TrackArtistLinkType::Remixer, - db::TrackArtistLinkType::Writer, - }; - _appearsOnReleaseContainer = {}; db::Release::FindParameters params; - params.setFilters(_filters.getDbFilters()); - params.setArtist(_artistId, types, { db::TrackArtistLinkType::ReleaseArtist }); + params.setTrackArtist(_artistId); params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc); - const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) }; - if (!releases.results.empty()) + db::Release::find(LmsApp->getDbSession(), params, [this](const db::Release::pointer& release) { + if (!release->hasArtist(_artistId)) + _appearsOnReleaseContainer.releases.push_back(release->getId()); + }); + + if (!_appearsOnReleaseContainer.releases.empty()) { Wt::WTemplate* releaseContainer{ bindNew("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) }; releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.Artist.appears-on")); - _appearsOnReleaseContainer.releases = releases.results; _appearsOnReleaseContainer.container = releaseContainer->bindNew("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); _appearsOnReleaseContainer.container->onRequestElements.connect(this, [this] { - addSomeReleases(_appearsOnReleaseContainer); + addSomeReleases(_appearsOnReleaseContainer, { releaseListHelpers::DisplayOptions::ShowYearAndOriginalYear, releaseListHelpers::DisplayOptions::ShowArtist }); }); } else @@ -343,7 +338,7 @@ namespace lms::ui } } - void Artist::addSomeReleases(ReleaseContainer& releaseContainer) + void Artist::addSomeReleases(ReleaseContainer& releaseContainer, core::EnumSet displayOptions) { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -352,7 +347,7 @@ namespace lms::ui for (std::size_t i{ static_cast(releaseContainer.container->getCount()) }; i < releaseContainer.releases.size(); ++i) { const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseContainer.releases[i]) }; - releaseContainer.container->add(releaseListHelpers::createEntryForArtist(release, artist)); + releaseContainer.container->add(releaseListHelpers::createEntry(release, displayOptions)); } } @@ -366,7 +361,6 @@ namespace lms::ui const db::Range range{ static_cast(_trackContainer->getCount()), _tracksBatchSize }; db::Track::FindParameters params; - params.setFilters(_filters.getDbFilters()); params.setArtist(_artistId); params.setRange(range); params.setSortMethod(db::TrackSortMethod::Name); @@ -390,5 +384,4 @@ namespace lms::ui return areTracksAdded; } - } // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 521cd03e..ebd8877c 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -21,11 +21,14 @@ #include +#include "core/EnumSet.hpp" + #include "database/Object.hpp" #include "database/objects/ArtistId.hpp" #include "database/objects/ArtworkId.hpp" #include "database/objects/ReleaseId.hpp" +#include "ReleaseHelpers.hpp" #include "ReleaseTypes.hpp" #include "common/Template.hpp" @@ -57,7 +60,7 @@ namespace lms::ui void refreshLinks(const db::ObjectPtr& artist); struct ReleaseContainer; - void addSomeReleases(ReleaseContainer& releaseContainer); + void addSomeReleases(ReleaseContainer& releaseContainer, core::EnumSet displayOptions); bool addSomeNonReleaseTracks(); static constexpr std::size_t _releasesBatchSize{ 6 }; static constexpr std::size_t _tracksBatchSize{ 6 }; @@ -76,5 +79,6 @@ namespace lms::ui ReleaseContainer _appearsOnReleaseContainer{}; InfiniteScrollingContainer* _trackContainer{}; db::ArtistId _artistId{}; + std::string _artistName; }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index 24ef72a6..2c19c98e 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -27,12 +27,14 @@ #include "database/objects/TrackArtistLink.hpp" #include "ArtistListHelpers.hpp" +#include "ArtistTypeSelector.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" #include "SortModeSelector.hpp" #include "State.hpp" -#include "TrackArtistLinkTypeSelector.hpp" #include "common/InfiniteScrollingContainer.hpp" +#include "database/objects/Types.hpp" +#include "explore/ArtistType.hpp" namespace lms::ui { @@ -61,13 +63,13 @@ namespace lms::ui } { - const std::optional linkType{ state::readValue("artists_link_type") }; - _artistCollector.setArtistLinkType(linkType); + ArtistType artistType{ state::readValue("artists_type").value_or(_defaultArtistType) }; + _artistCollector.setArtistType(artistType); - TrackArtistLinkTypeSelector* linkTypeSelector{ bindNew("link-type", linkType) }; - linkTypeSelector->itemSelected.connect([this](std::optional newLinkType) { - state::writeValue("artists_link_type", newLinkType); - refreshView(newLinkType); + ArtistTypeSelector* artistTypeSelector{ bindNew("artist-type", artistType) }; + artistTypeSelector->itemSelected.connect([this](ArtistType newArtistType) { + state::writeValue("artists_type", newArtistType); + refreshView(newArtistType); }); } @@ -95,9 +97,9 @@ namespace lms::ui refreshView(); } - void Artists::refreshView(std::optional linkType) + void Artists::refreshView(ArtistType artistType) { - _artistCollector.setArtistLinkType(linkType); + _artistCollector.setArtistType(artistType); refreshView(); } diff --git a/src/lms/ui/explore/ArtistsView.hpp b/src/lms/ui/explore/ArtistsView.hpp index 4ef4c38f..fe3f3839 100644 --- a/src/lms/ui/explore/ArtistsView.hpp +++ b/src/lms/ui/explore/ArtistsView.hpp @@ -19,14 +19,11 @@ #pragma once -#include - #include #include -#include "database/Types.hpp" - #include "ArtistCollector.hpp" +#include "ArtistType.hpp" #include "common/Template.hpp" namespace lms::ui @@ -42,7 +39,7 @@ namespace lms::ui private: void refreshView(); void refreshView(ArtistCollector::Mode mode); - void refreshView(std::optional linkType); + void refreshView(ArtistType artistType); void refreshView(const Wt::WString& searchText); void addSome(); @@ -53,5 +50,6 @@ namespace lms::ui InfiniteScrollingContainer* _container{}; ArtistCollector _artistCollector; static constexpr ArtistCollector::Mode _defaultSortMode{ ArtistCollector::Mode::Random }; + static constexpr ArtistType _defaultArtistType{ ReleaseArtistsTag{} }; }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ReleaseHelpers.cpp b/src/lms/ui/explore/ReleaseHelpers.cpp index 7c3a3327..d5ca2c3e 100644 --- a/src/lms/ui/explore/ReleaseHelpers.cpp +++ b/src/lms/ui/explore/ReleaseHelpers.cpp @@ -34,80 +34,53 @@ namespace lms::ui::releaseListHelpers { - namespace + std::unique_ptr createEntry(const db::Release::pointer& release, core::EnumSet options) { - enum class ReleaseOptions + auto entry{ std::make_unique(Wt::WString::tr("Lms.Explore.Releases.template.entry-grid")) }; + + entry->bindWidget("release-name", utils::createReleaseAnchor(release)); + entry->addFunction("tr", &Wt::WTemplate::Functions::tr); + { - ShowArtist, - ShowYearAndOriginalYear, - ShowYear, - }; + Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) }; - std::unique_ptr createEntryInternal(const db::Release::pointer& release, const db::Artist::pointer& artist, core::EnumSet options) - { - auto entry{ std::make_unique(Wt::WString::tr("Lms.Explore.Releases.template.entry-grid")) }; + std::unique_ptr image; + if (release->getPreferredArtworkId().isValid()) + image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large); + else + image = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release); - entry->bindWidget("release-name", utils::createReleaseAnchor(release)); - entry->addFunction("tr", &Wt::WTemplate::Functions::tr); - - { - Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) }; - - std::unique_ptr image; - if (release->getPreferredArtworkId().isValid()) - image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large); - else - image = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release); - - image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack - anchor->setImage(std::move(image)); - } - - if (options.contains(ReleaseOptions::ShowArtist)) - { - auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : db::ArtistId{}, "link-secondary") }; - if (artistAnchors) - { - entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", std::move(artistAnchors)); - } - } - - if (options.contains(ReleaseOptions::ShowYearAndOriginalYear)) - { - Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) }; - if (!year.empty()) - { - entry->setCondition("if-has-year", true); - entry->bindString("year", year, Wt::TextFormat::Plain); - } - } - else if (options.contains(ReleaseOptions::ShowYear)) - { - if (release->getYear()) - { - entry->setCondition("if-has-year", true); - entry->bindInt("year", *release->getYear()); - } - } - - return entry; + image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack + anchor->setImage(std::move(image)); } - } // namespace - std::unique_ptr createEntry(const db::Release::pointer& release) - { - return createEntryInternal(release, db::Artist::pointer{}, core::EnumSet{ ReleaseOptions::ShowArtist }); - } + if (options.contains(DisplayOptions::ShowArtist)) + { + utils::ArtistDisplayInfo artistDisplayInfo{ utils::computeArtistDisplayInfo(release) }; - std::unique_ptr createEntryForArtist(const db::Release::pointer& release, const db::Artist::pointer& artist) - { - return createEntryInternal(release, artist, core::EnumSet{ ReleaseOptions::ShowArtist, ReleaseOptions::ShowYearAndOriginalYear }); - } + entry->setCondition("if-has-artist", true); + entry->bindWidget("artist-name", utils::createArtistsAnchors(artistDisplayInfo, "link-secondary")); + } - std::unique_ptr createEntryForOtherVersions(const db::ObjectPtr& release) - { - return createEntryInternal(release, db::Artist::pointer{}, core::EnumSet{ ReleaseOptions::ShowYear }); + if (options.contains(DisplayOptions::ShowYearAndOriginalYear)) + { + Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) }; + if (!year.empty()) + { + entry->setCondition("if-has-year", true); + entry->bindString("year", year, Wt::TextFormat::Plain); + } + } + else if (options.contains(DisplayOptions::ShowYear)) + { + if (release->getYear()) + { + entry->setCondition("if-has-year", true); + entry->bindInt("year", *release->getYear()); + } + } + + return entry; } } // namespace lms::ui::releaseListHelpers diff --git a/src/lms/ui/explore/ReleaseHelpers.hpp b/src/lms/ui/explore/ReleaseHelpers.hpp index 4bd0eea2..169b6488 100644 --- a/src/lms/ui/explore/ReleaseHelpers.hpp +++ b/src/lms/ui/explore/ReleaseHelpers.hpp @@ -38,9 +38,14 @@ namespace lms::db namespace lms::ui::releaseListHelpers { - std::unique_ptr createEntry(const db::ObjectPtr& release); - std::unique_ptr createEntryForArtist(const db::ObjectPtr& release, const db::ObjectPtr& artist); - std::unique_ptr createEntryForOtherVersions(const db::ObjectPtr& release); + enum class DisplayOptions + { + ShowArtist, + ShowYearAndOriginalYear, + ShowYear, + }; + + std::unique_ptr createEntry(const db::ObjectPtr& release, core::EnumSet options); } // namespace lms::ui::releaseListHelpers namespace lms::ui::releaseHelpers diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index f23ee427..2405c240 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -78,53 +78,14 @@ namespace lms::ui releaseInfo->bindString("release-type", releaseHelpers::buildReleaseTypeString(parseReleaseType(releaseTypeNames))); } - std::map> artistMap; - - auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) { - db::Artist::FindParameters params; - params.setRelease(releaseId); - params.setLinkType(linkType); - const auto artistIds{ db::Artist::findIds(LmsApp->getDbSession(), params) }; - if (artistIds.results.empty()) - return; - - Wt::WString typeStr{ Wt::WString::trn(type, artistIds.results.size()) }; - for (db::ArtistId artistId : artistIds.results) - artistMap[typeStr].insert(artistId); - }; - - auto addPerformerArtists = [&] { - db::TrackArtistLink::FindParameters params; - params.setRelease(releaseId); - params.setLinkType(db::TrackArtistLinkType::Performer); - db::TrackArtistLink::find(LmsApp->getDbSession(), params, [&](const db::TrackArtistLink::pointer& link) { - artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId()); - }); - }; - - addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer"); - addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor"); - addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist"); - addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer"); - addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer"); - addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer"); - addPerformerArtists(); - - if (auto itRolelessPerformers{ artistMap.find("") }; itRolelessPerformers != std::cend(artistMap)) - { - Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size()) }; - artistMap[performersStr] = std::move(itRolelessPerformers->second); - artistMap.erase(itRolelessPerformers); - } - - if (!artistMap.empty()) + if (const auto artistsByRole{ utils::getTrackArtistsByRole(release) }; !artistsByRole.empty()) { releaseInfo->setCondition("if-has-artist", true); Wt::WContainerWidget* artistTable{ releaseInfo->bindNew("artist-table") }; - for (const auto& [role, artistIds] : artistMap) + for (const auto& [role, artists] : artistsByRole) { - std::unique_ptr artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) }; + std::unique_ptr artistContainer{ utils::createArtistAnchorList(artists) }; auto artistsEntry{ std::make_unique