diff --git a/approot/messages.xml b/approot/messages.xml index 058d29e0..9ba2dc91 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -158,6 +158,7 @@ Duration Filter added Filters +Music Library Most played MusicBrainz Artist MusicBrainz Release diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 77da8540..54b33a6f 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -158,6 +158,7 @@ Durée Filtre ajouté Filtres +Bibliothèque musicale Plus joués MusicBrainz Artist MusicBrainz Release diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 8a2da532..409b048c 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -110,6 +110,7 @@ Scansione: passo {1}/{2} Controllo duplicati... {1} files Controllo file... {1}% +Compattazione del database... Calcolo statistiche... {1}% File trovati: {1} files Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)... @@ -157,6 +158,7 @@ Durata Filtro aggiunto Filtri +Libreria musicale Più ascoltati Artista MusicBrainz Album MusicBrainz diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 05774126..745cfe82 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -158,6 +158,7 @@ 过滤器已添加 过滤器 + 播放最多 MusicBrainz 歌手 MusicBrainz 专辑 diff --git a/src/libs/database/impl/Cluster.cpp b/src/libs/database/impl/Cluster.cpp index 7c9a3d3f..ee1dab9b 100644 --- a/src/libs/database/impl/Cluster.cpp +++ b/src/libs/database/impl/Cluster.cpp @@ -43,17 +43,19 @@ namespace lms::db query.groupBy("c.id"); if (params.track.isValid() || params.release.isValid()) - { query.join("track_cluster t_c ON t_c.cluster_id = c.id"); - query.join("track t ON t.id = t_c.track_id"); - } + if (!params.clusterTypeName.empty()) query.join("cluster_type c_t ON c_t.id = c.cluster_type_id"); if (params.track.isValid()) - query.where("t.id = ?").bind(params.track); + query.where("t_c.track_id = ?").bind(params.track); + if (params.release.isValid()) + { + query.join("track t ON t.id = t_c.track_id"); query.where("t.release_id = ?").bind(params.release); + } assert(!params.clusterType.isValid() || params.clusterTypeName.empty()); if (params.clusterType.isValid()) @@ -61,6 +63,17 @@ namespace lms::db else if (!params.clusterTypeName.empty()) query.where("c_t.name = ?").bind(params.clusterTypeName); + switch (params.sortMethod) + { + case ClusterSortMethod::None: + break; + case ClusterSortMethod::Name: + query.orderBy("c.name COLLATE NOCASE"); + break; + } + + query.groupBy("c.id"); + return query; } @@ -208,6 +221,12 @@ namespace lms::db return utils::execRangeQuery(query, range); } + void ClusterType::find(Session& session, const std::function& func) + { + auto query{ session.getDboSession()->find() }; + return utils::forEachQueryResult(query, func); + } + ClusterType::pointer ClusterType::find(Session& session, std::string_view name) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index 426de394..5db01f67 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -34,7 +34,7 @@ namespace lms::db { namespace { - static constexpr Version LMS_DATABASE_VERSION{ 57 }; + static constexpr Version LMS_DATABASE_VERSION{ 58 }; } VersionInfo::VersionInfo() @@ -455,11 +455,17 @@ SELECT void migrateFromV56(Session& session) { // Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all - std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; + std::vector indexeNames{ utils::fetchQueryResults(session.getDboSession()->query(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) }; for (const auto& indexName : indexeNames) session.getDboSession()->execute("DROP INDEX " + indexName); } + void migrateFromV57(Session& session) + { + // useless index + session.getDboSession()->execute("DROP INDEX cluster_name_idx"); + } + bool doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -494,6 +500,7 @@ SELECT {54, migrateFromV54}, {55, migrateFromV55}, {56, migrateFromV56}, + {57, migrateFromV57}, }; bool migrationPerformed{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 342d8cb4..8201588c 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -172,7 +172,6 @@ namespace lms::db _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index d97e74a8..aa6d85db 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -35,6 +35,79 @@ namespace lms::db { + namespace + { + template + Wt::Dbo::Query createQuery(Session& session, std::string_view itemToSelect, const TrackList::FindParameters& params) + { + auto query{ session.getDboSession()->query("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM tracklist t_l") }; + + if (!params.clusters.empty() || params.mediaLibrary.isValid()) + query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); + + if (params.mediaLibrary.isValid()) + query.join("track t ON t.id = t_l_e.track_id"); + + if (params.mediaLibrary.isValid()) + query.where("t.media_library_id = ?").bind(params.mediaLibrary); + + if (params.user.isValid()) + query.where("t_l.user_id = ?").bind(params.user); + + if (params.type) + query.where("t_l.type = ?").bind(*params.type); + + if (!params.clusters.empty()) + { + std::ostringstream oss; + oss << "t_l_e.track_id IN (SELECT DISTINCT t.id FROM track t" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id"; + + WhereClause clusterClause; + for (const ClusterId clusterId : params.clusters) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(clusterId); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; + + query.where(oss.str()); + } + + switch (params.sortMethod) + { + case TrackListSortMethod::None: + break; + case TrackListSortMethod::Name: + query.orderBy("t_l.name COLLATE NOCASE"); + break; + case TrackListSortMethod::LastModifiedDesc: + query.orderBy("t_l.last_modified_date_time DESC"); + break; + } + + return query; + } + + template + Wt::Dbo::Query createQuery(Session& session, const TrackList::FindParameters& params) + { + std::string_view itemToSelect; + + if constexpr (std::is_same_v) + itemToSelect = "t_l.id"; + else if constexpr (std::is_same_v>) + itemToSelect = "t_l"; + else + static_assert("Unhandled type"); + + return createQuery(session, itemToSelect, params); + } + } + TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr user) : _name{ name } , _type{ type } @@ -58,7 +131,6 @@ namespace lms::db return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM tracklist")); } - TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId) { session.checkReadTransaction(); @@ -73,53 +145,17 @@ namespace lms::db RangeResults TrackList::find(Session& session, const FindParameters& params) { session.checkReadTransaction(); - - auto query{ session.getDboSession()->query("SELECT DISTINCT t_l.id FROM tracklist t_l") }; - - if (params.user.isValid()) - query.where("t_l.user_id = ?").bind(params.user); - - if (params.type) - query.where("t_l.type = ?").bind(*params.type); - - if (!params.clusters.empty()) - { - query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); - query.join("track t ON t.id = t_l_e.track_id"); - - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" - " INNER JOIN cluster c ON c.id = t_c.cluster_id"; - - WhereClause clusterClause; - for (const ClusterId clusterId : params.clusters) - { - clusterClause.Or(WhereClause("c.id = ?")); - query.bind(clusterId); - } - - oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; - - query.where(oss.str()); - } - - switch (params.sortMethod) - { - case TrackListSortMethod::None: - break; - case TrackListSortMethod::Name: - query.orderBy("t_l.name COLLATE NOCASE"); - break; - case TrackListSortMethod::LastModifiedDesc: - query.orderBy("t_l.last_modified_date_time DESC"); - break; - } - + auto query{ createQuery(session, params) }; return utils::execRangeQuery(query, params.range); } + void TrackList::find(Session& session, const FindParameters& params, const std::function& func) + { + session.checkReadTransaction(); + auto query{ createQuery>(session, params) }; + utils::forEachQueryRangeResult(query, params.range, func); + } + TrackList::pointer TrackList::find(Session& session, TrackListId id) { session.checkReadTransaction(); diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index 27994799..12f81161 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include #include @@ -67,7 +68,7 @@ namespace lms::db ReleaseId release; // artists involved in this release MediaLibraryId mediaLibrary; // artists that belong to this library - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setLinkType(std::optional _linkType) { linkType = _linkType; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } diff --git a/src/libs/database/include/database/Cluster.hpp b/src/libs/database/include/database/Cluster.hpp index 4408d2c2..fbdc1a72 100644 --- a/src/libs/database/include/database/Cluster.hpp +++ b/src/libs/database/include/database/Cluster.hpp @@ -46,12 +46,14 @@ namespace lms::db struct FindParameters { std::optional range; + ClusterSortMethod sortMethod; ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type std::string clusterTypeName; // if non empty, clusters that belong to this cluster type TrackId track; // if set, clusters involved in this track ReleaseId release; // if set, clusters involved in this release FindParameters& setRange(std::optional _range) { range = _range; return *this; } + FindParameters& setSortMethod(ClusterSortMethod _method) { sortMethod = _method; return *this; } FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; } FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } @@ -119,6 +121,7 @@ namespace lms::db // Getters static std::size_t getCount(Session& session); static RangeResults findIds(Session& session, std::optional range = std::nullopt); + static void find(Session& session, const std::function& func); static pointer find(Session& session, std::string_view name); static pointer find(Session& session, ClusterTypeId id); static RangeResults findOrphanIds(Session& session, std::optional range = std::nullopt); diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp index 2933d481..6bb91457 100644 --- a/src/libs/database/include/database/Release.hpp +++ b/src/libs/database/include/database/Release.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -96,7 +97,7 @@ namespace lms::db std::string releaseType; // If set, albums that has this release type MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } FindParameters& setRange(std::optional _range) { range = _range; return *this; } diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 84cde313..9d197411 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -80,7 +81,7 @@ namespace lms::db std::optional discNumber; // matching this disc number MediaLibraryId mediaLibrary; // If set, tracks in this library - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } FindParameters& setName(std::string_view _name) { name = _name; return *this; } FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } diff --git a/src/libs/database/include/database/TrackList.hpp b/src/libs/database/include/database/TrackList.hpp index bdeff6bb..91dc92e1 100644 --- a/src/libs/database/include/database/TrackList.hpp +++ b/src/libs/database/include/database/TrackList.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include "database/ClusterId.hpp" +#include "database/MediaLibraryId.hpp" #include "database/Object.hpp" #include "database/TrackId.hpp" #include "database/TrackListId.hpp" @@ -57,18 +59,21 @@ namespace lms::db std::optional range; std::optional type; UserId user; // only tracklists owned by this user + MediaLibraryId mediaLibrary; // only tracklists that have songs in this media library TrackListSortMethod sortMethod{ TrackListSortMethod::None }; - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setType(TrackListType _type) { type = _type; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary) { mediaLibrary = _mediaLibrary; return *this; } FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } }; static std::size_t getCount(Session& session); static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId); static pointer find(Session& session, TrackListId tracklistId); static RangeResults find(Session& session, const FindParameters& params); + static void find(Session& session, const FindParameters& params, const std::function& func); // Accessors std::string_view getName() const { return _name; } diff --git a/src/libs/database/include/database/Types.hpp b/src/libs/database/include/database/Types.hpp index 506355b4..17b6bd27 100644 --- a/src/libs/database/include/database/Types.hpp +++ b/src/libs/database/include/database/Types.hpp @@ -116,6 +116,12 @@ namespace lms::db StarredDateDesc, }; + enum class ClusterSortMethod + { + None, + Name, + }; + enum class ReleaseSortMethod { None, diff --git a/src/libs/database/test/Cluster.cpp b/src/libs/database/test/Cluster.cpp index 027cc14b..edb28da2 100644 --- a/src/libs/database/test/Cluster.cpp +++ b/src/libs/database/test/Cluster.cpp @@ -137,11 +137,11 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) }; + auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters(std::initializer_list{cluster1.getId()})) }; ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::findIds(session, Track::FindParameters{}.setClusters({ cluster2.getId() })); + tracks = Track::findIds(session, Track::FindParameters{}.setClusters(std::initializer_list{ cluster2.getId() })); EXPECT_EQ(tracks.results.size(), 0); } @@ -237,6 +237,35 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, ClusterType) + { + { + auto transaction{ session.createReadTransaction() }; + + bool visited{}; + ClusterType::find(session, [&](const ClusterType::pointer&) + { + visited = true; + }); + EXPECT_FALSE(visited); + } + + ScopedClusterType clusterType1{ session, "MyClusterType1" }; + ScopedClusterType clusterType2{ session, "MyClusterType2" }; + + { + auto transaction{ session.createReadTransaction() }; + + std::vector visitedClusterTypes; + ClusterType::find(session, [&](const ClusterType::pointer& clusterType) + { + visitedClusterTypes.push_back(clusterType->getId()); + }); + ASSERT_EQ(visitedClusterTypes.size(), 2); + EXPECT_EQ(visitedClusterTypes[0], clusterType1->getId()); + EXPECT_EQ(visitedClusterTypes[1], clusterType2->getId()); + } + } TEST_F(DatabaseFixture, ClusterType_singleTrack) { @@ -285,7 +314,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2); - EXPECT_EQ(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.size(), 0); + EXPECT_EQ(Release::find(session, Release::FindParameters{}.setClusters(std::initializer_list{ unusedCluster.getId() })).results.size(), 0); EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0); @@ -321,7 +350,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({cluster.getId()})) }; + const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters(std::initializer_list{cluster.getId()})) }; ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -329,7 +358,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) }; + const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters(std::initializer_list{unusedCluster.getId()})) }; EXPECT_EQ(releases.results.size(), 0); } @@ -375,12 +404,12 @@ namespace lms::db::tests { auto transaction{ session.createWriteTransaction() }; - auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) }; + auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list{cluster1.getId()})) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })).results.size(), 0); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list{ cluster2.getId() })).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list{ cluster3.getId() })).results.size(), 0); cluster2.get().modify()->addTrack(track.get()); } @@ -388,19 +417,19 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) }; + auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list{cluster1.getId()})) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster2.getId() })); + artists = Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list{ cluster2.getId() })); ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster1.getId() })); + artists = Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list{ cluster1.getId() })); ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters({ cluster3.getId() })).results.size(), 0); + EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setClusters(std::initializer_list{ cluster3.getId() })).results.size(), 0); } } @@ -429,7 +458,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) }; + auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list{cluster.getId()})) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); } @@ -581,7 +610,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) }; + auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters(std::initializer_list{cluster.getId()})) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results.front(), artist.getId()); @@ -589,7 +618,7 @@ namespace lms::db::tests ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster.getId() })); + releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list{ cluster.getId() })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } @@ -620,7 +649,7 @@ namespace lms::db::tests ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters({ cluster1.getId(), cluster2.getId() })); + releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setClusters(std::initializer_list{ cluster1.getId(), cluster2.getId() })); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.front(), release.getId()); } diff --git a/src/libs/database/test/TrackList.cpp b/src/libs/database/test/TrackList.cpp index 5473e13e..091e4cc3 100644 --- a/src/libs/database/test/TrackList.cpp +++ b/src/libs/database/test/TrackList.cpp @@ -151,6 +151,46 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, TrackList_MediaLibrary) + { + ScopedUser user{ session, "MyUser" }; + ScopedTrackList trackList1{ session, "MytrackList1", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrackList trackList2{ session, "MytrackList2", TrackListType::Playlist, false, user.lockAndGet() }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + ScopedMediaLibrary library{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + session.create(track1.get(), trackList1.get()); + session.create(track2.get(), trackList2.get()); + track2.get().modify()->setMediaLibrary(library.get()); + } + + { + auto transaction{ session.createReadTransaction() }; + std::vector visitedTrackLists; + TrackList::find(session, TrackList::FindParameters{}, [&](const TrackList::pointer& trackList) + { + visitedTrackLists.push_back(trackList->getId()); + }); + ASSERT_EQ(visitedTrackLists.size(), 2); + EXPECT_EQ(visitedTrackLists[0], trackList1->getId()); + EXPECT_EQ(visitedTrackLists[1], trackList2->getId()); + } + + { + auto transaction{ session.createReadTransaction() }; + std::vector visitedTrackLists; + TrackList::find(session, TrackList::FindParameters{}.setMediaLibrary(library->getId()), [&](const TrackList::pointer& trackList) + { + visitedTrackLists.push_back(trackList->getId()); + }); + ASSERT_EQ(visitedTrackLists.size(), 1); + EXPECT_EQ(visitedTrackLists[0], trackList2->getId()); + } + } + TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) { ScopedUser user{ session, "MyUser" }; @@ -163,7 +203,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters(std::initializer_list{cluster.getId()})) }; EXPECT_EQ(trackLists.results.size(), 0); } @@ -177,7 +217,7 @@ namespace lms::db::tests { auto transaction{ session.createReadTransaction() }; - auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) }; + auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters(std::initializer_list{cluster.getId()})) }; ASSERT_EQ(trackLists.results.size(), 1); EXPECT_EQ(trackLists.results.front(), trackList1.getId()); } diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp index 04a4ae8a..0f39cdb2 100644 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -57,7 +58,7 @@ namespace lms::feedback db::MediaLibraryId library; FindParameters& setUser(const db::UserId _user) { user = _user; return *this; } - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; } }; diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index f61162cb..2b73770e 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -65,7 +66,7 @@ namespace lms::scrobbling db::ArtistId artist; // if set, match this artist FindParameters& setUser(const db::UserId _user) { user = _user; return *this; } - FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setClusters(std::span _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; } FindParameters& setRange(std::optional _range) { range = _range; return *this; } FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; } FindParameters& setArtist(db::ArtistId _artist) { artist = _artist; return *this; } diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 33495a61..7cfc60ab 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -88,7 +88,7 @@ namespace lms::api::subsonic if (const Cluster::pointer cluster{ clusterType->getCluster(genre) }) { Release::FindParameters params; - params.setClusters({ cluster->getId() }); + params.setClusters(std::initializer_list{ cluster->getId() }); params.setSortMethod(ReleaseSortMethod::Name); params.setRange(range); params.setMediaLibrary(mediaLibraryId); @@ -280,7 +280,7 @@ namespace lms::api::subsonic Response::Node& songsByGenreNode{ response.createNode("songsByGenre") }; Track::FindParameters params; - params.setClusters({ cluster->getId() }); + params.setClusters(std::initializer_list{ cluster->getId() }); params.setRange(Range{ offset, count }); params.setMediaLibrary(mediaLibrary); diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index d00c670f..d12f7889 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -87,9 +87,7 @@ namespace lms::ui return trackListModel->getValue(*row); } - static - std::shared_ptr - createTrackListModel() + static std::shared_ptr createTrackListModel() { using namespace db; @@ -102,12 +100,10 @@ namespace lms::ui params.setUser(LmsApp->getUserId()); params.setSortMethod(TrackListSortMethod::Name); - auto tracklists{ TrackList::find(LmsApp->getDbSession(), params) }; - for (const TrackListId trackListId : tracklists.results) - { - const TrackList::pointer trackList{ TrackList::find(LmsApp->getDbSession(), trackListId) }; - model->add(Wt::WString::fromUTF8(std::string{ trackList->getName() }), trackListId); - } + TrackList::find(LmsApp->getDbSession(), params, [&](const TrackList::pointer& trackList) + { + model->add(Wt::WString::fromUTF8(std::string{ trackList->getName() }), trackList->getId()); + }); return model; } diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 86d0dfaf..cc36f3fa 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -80,7 +80,18 @@ namespace lms::ui::utils return cover; } - std::unique_ptr createCluster(db::ClusterId clusterId, bool canDelete) + std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete) + { + auto res{ std::make_unique(Wt::WString{ canDelete ? " " : "" } + name, Wt::TextFormat::UnsafeXHTML) }; + + res->setStyleClass("Lms-badge-cluster badge me-1 " + std::string{ colorStyleClass }); // HACK + res->setToolTip(tooltip, Wt::TextFormat::Plain); + res->setInline(true); + + return res; + } + + std::unique_ptr createFilterCluster(db::ClusterId clusterId, bool canDelete) { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; @@ -88,7 +99,7 @@ namespace lms::ui::utils if (!cluster) return {}; - auto getStyleClass{ [](const db::Cluster::pointer& cluster) -> const char* + auto getStyleClass{ [](const db::Cluster::pointer& cluster) -> std::string_view { switch (cluster->getType()->getId().getValue() % 8) { @@ -105,14 +116,33 @@ namespace lms::ui::utils return "bg-primary"; } }; - const std::string styleClass{ getStyleClass(cluster) }; - auto res{ std::make_unique(std::string {} + (canDelete ? " " : "") + Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::TextFormat::UnsafeXHTML) }; + return createFilter(Wt::WString::fromUTF8(std::string{ cluster->getName() }), Wt::WString::fromUTF8(std::string{ cluster->getType()->getName() }), getStyleClass(cluster), canDelete); + } - res->setStyleClass("Lms-badge-cluster badge me-1 " + styleClass); // HACK - res->setToolTip(std::string{ cluster->getType()->getName() }, Wt::TextFormat::Plain); - res->setInline(true); + std::unique_ptr createFilterClustersForTrack(db::Track::pointer track, Filters& filters) + { + using namespace db; - return res; + std::unique_ptr clusterContainer{ std::make_unique() }; + + // TODO: optimize this + const auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results }; + const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) }; + + for (const auto& clusters : clusterGroups) + { + for (const Cluster::pointer& cluster : clusters) + { + const ClusterId clusterId{ cluster->getId() }; + Wt::WInteractWidget* entry{ clusterContainer->addWidget(createFilterCluster(clusterId)) }; + entry->clicked().connect([&filters, clusterId] + { + filters.add(clusterId); + }); + } + } + + return clusterContainer; } std::unique_ptr createArtistAnchorList(const std::vector& artistIds, std::string_view cssAnchorClass) @@ -278,30 +308,4 @@ namespace lms::ui::utils return res; } - - std::unique_ptr createClustersForTrack(db::Track::pointer track, Filters& filters) - { - using namespace db; - - std::unique_ptr clusterContainer{ std::make_unique() }; - - // TODO: optimize this - const auto clusterTypes{ ClusterType::findIds(LmsApp->getDbSession()).results }; - const auto clusterGroups{ track->getClusterGroups(clusterTypes, 3) }; - - for (const auto& clusters : clusterGroups) - { - for (const Cluster::pointer& cluster : clusters) - { - const ClusterId clusterId{ cluster->getId() }; - Wt::WInteractWidget* entry{ clusterContainer->addWidget(createCluster(clusterId)) }; - entry->clicked().connect([&filters, clusterId] - { - filters.add(clusterId); - }); - } - } - - return clusterContainer; - } } diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 3b79acf0..f1db77c1 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "database/ArtistId.hpp" #include "database/ClusterId.hpp" @@ -56,7 +57,9 @@ namespace lms::ui::utils std::unique_ptr createCover(db::ReleaseId releaseId, CoverResource::Size size); std::unique_ptr createCover(db::TrackId trackId, CoverResource::Size size); - std::unique_ptr createCluster(db::ClusterId clusterId, bool canDelete = false); + std::unique_ptr createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false); + 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"); @@ -67,6 +70,4 @@ namespace lms::ui::utils 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 createClustersForTrack(db::ObjectPtr track, Filters& filters); } diff --git a/src/lms/ui/common/ValueStringModel.hpp b/src/lms/ui/common/ValueStringModel.hpp index c0fe922a..39682bd9 100644 --- a/src/lms/ui/common/ValueStringModel.hpp +++ b/src/lms/ui/common/ValueStringModel.hpp @@ -23,60 +23,55 @@ #include -namespace lms::ui { - -// Helper class -template -class ValueStringModel : public Wt::WStringListModel +namespace lms::ui { - public: - T getValue(std::size_t row) const - { - return Wt::cpp17::any_cast(data(index(static_cast(row), 0), Wt::ItemDataRole::User)); - } + // Helper class + template + class ValueStringModel : public Wt::WStringListModel + { + public: + T getValue(std::size_t row) const + { + return Wt::cpp17::any_cast(data(index(static_cast(row), 0), Wt::ItemDataRole::User)); + } - Wt::WString getString(std::size_t row) const - { - return Wt::cpp17::any_cast(data(index(static_cast(row), 0), Wt::ItemDataRole::Display)); - } + Wt::WString getString(std::size_t row) const + { + return Wt::cpp17::any_cast(data(index(static_cast(row), 0), Wt::ItemDataRole::Display)); + } - std::optional - getRowFromString(const Wt::WString& value) - { - for (std::size_t i{}; i < static_cast(rowCount()); ++i) - { - if (getString(i) == value) - return i; - } + std::optional getRowFromString(const Wt::WString& value) + { + for (std::size_t i{}; i < static_cast(rowCount()); ++i) + { + if (getString(i) == value) + return i; + } - return std::nullopt; - } + return std::nullopt; + } - std::optional - getRowFromValue(const T& value) - { - for (std::size_t i{}; i < static_cast(rowCount()); ++i) - { - if (getValue(i) == value) - return i; - } + std::optional getRowFromValue(const T& value) + { + for (std::size_t i{}; i < static_cast(rowCount()); ++i) + { + if (getValue(i) == value) + return i; + } - return std::nullopt; - } + return std::nullopt; + } - void - add(const Wt::WString& str, const T& value) - { - insertRows(rowCount(), 1); - setData(rowCount() - 1, 0, value, Wt::ItemDataRole::User); - setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display); - } - - void - clear() - { - removeRows(0, rowCount()); - } -}; + void add(const Wt::WString& str, const T& value) + { + insertRows(rowCount(), 1); + setData(rowCount() - 1, 0, value, Wt::ItemDataRole::User); + setData(rowCount() - 1, 0, str, Wt::ItemDataRole::Display); + } + void clear() + { + removeRows(0, rowCount()); + } + }; } // namespace lms::ui diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index d927c86b..097b63a3 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -39,6 +39,7 @@ namespace lms::ui scrobbling::IScrobblingService& scrobblingService{ *core::Service::get() }; const Range range{ getActualRange(requestedRange) }; + const Filters& filters{ getFilters() }; RangeResults artists; @@ -52,7 +53,8 @@ namespace lms::ui { feedback::IFeedbackService::ArtistFindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); params.setSortMethod(ArtistSortMethod::StarredDateDesc); params.setRange(range); @@ -64,7 +66,8 @@ namespace lms::ui { scrobbling::IScrobblingService::ArtistFindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); params.setRange(range); @@ -76,7 +79,8 @@ namespace lms::ui { scrobbling::IScrobblingService::ArtistFindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); params.setRange(range); @@ -87,7 +91,8 @@ namespace lms::ui case Mode::RecentlyAdded: { Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); params.setSortMethod(ArtistSortMethod::LastWritten); params.setRange(range); @@ -102,7 +107,8 @@ namespace lms::ui case Mode::Search: { Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setKeywords(getSearchKeywords()); params.setLinkType(_linkType); params.setRange(range); @@ -117,7 +123,8 @@ namespace lms::ui case Mode::All: { Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setLinkType(_linkType); params.setSortMethod(ArtistSortMethod::SortName); params.setRange(range); @@ -143,7 +150,8 @@ namespace lms::ui if (!_randomArtists) { Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setLinkType(_linkType); params.setSortMethod(ArtistSortMethod::Random); params.setRange(Range{ 0, getMaxCount() }); diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index f8e31405..6c6b9d23 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -138,7 +138,7 @@ namespace lms::ui for (const db::Cluster::pointer& cluster : clusters) { const db::ClusterId clusterId = cluster->getId(); - Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createCluster(clusterId)) }; + Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterCluster(clusterId)) }; entry->clicked().connect([this, clusterId] { _filters.add(clusterId); @@ -198,7 +198,8 @@ namespace lms::ui _releaseContainers.clear(); Release::FindParameters params; - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); params.setArtist(_artistId, { TrackArtistLinkType::ReleaseArtist }, {}); params.setSortMethod(ReleaseSortMethod::OriginalDateDesc); @@ -257,7 +258,8 @@ namespace lms::ui _appearsOnReleaseContainer = {}; Release::FindParameters params; - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); params.setArtist(_artistId, types, { TrackArtistLinkType::ReleaseArtist }); params.setSortMethod(ReleaseSortMethod::OriginalDateDesc); @@ -343,7 +345,8 @@ namespace lms::ui const Range range{ static_cast(_trackContainer->getCount()), _tracksBatchSize }; Track::FindParameters params; - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); params.setArtist(_artistId); params.setRange(range); params.setSortMethod(TrackSortMethod::Name); diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index 93f3bb21..dddd039e 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -58,7 +58,7 @@ namespace lms::ui protected: Range getActualRange(std::optional range) const; std::size_t getMaxCount() const; - Filters& getFilters() { return _filters; } + const Filters& getFilters() { return _filters; } const std::vector& getSearchKeywords() const { return _searchKeywords; } private: diff --git a/src/lms/ui/explore/Filters.cpp b/src/lms/ui/explore/Filters.cpp index f7ee3ff9..1062b715 100644 --- a/src/lms/ui/explore/Filters.cpp +++ b/src/lms/ui/explore/Filters.cpp @@ -19,21 +19,79 @@ #include "Filters.hpp" +#include + #include #include #include #include #include "database/Cluster.hpp" +#include "database/MediaLibrary.hpp" #include "database/Session.hpp" +#include "common/ValueStringModel.hpp" #include "LmsApplication.hpp" #include "Utils.hpp" #include "ModalManager.hpp" -namespace lms::ui +namespace lms::ui { - using namespace db; + namespace + { + struct MediaLibraryTag {}; + + using TypeVariant = std::variant; + using TypeModel = ValueStringModel; + + std::unique_ptr createTypeModel() + { + auto typeModel{ std::make_unique() }; + typeModel->add(Wt::WString::tr("Lms.Explore.media-library"), MediaLibraryTag{}); + + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + + db::ClusterType::find(LmsApp->getDbSession(), [&](const db::ClusterType::pointer& clusterType) + { + typeModel->add(Wt::WString::fromUTF8(std::string{ clusterType->getName() }), clusterType->getId()); + }); + + return typeModel; + } + + using ValueVariant = std::variant; + using ValueModel = ValueStringModel; + + std::unique_ptr createValueModel(TypeVariant type) + { + db::Session& session{ LmsApp->getDbSession() }; + + auto valueModel{ std::make_unique() }; + + auto transaction{ session.createReadTransaction() }; + + if (std::holds_alternative(type)) + { + db::MediaLibrary::find(session, [&](const db::MediaLibrary::pointer& library) + { + valueModel->add(Wt::WString::fromUTF8(std::string{ library->getName() }), library->getId()); + }); + } + else if (const db::ClusterTypeId * clusterTypeId{ std::get_if(&type) }) + { + db::Cluster::FindParameters params; + params.setClusterType(*clusterTypeId); + params.setSortMethod(db::ClusterSortMethod::Name); + + db::Cluster::find(session, params, [&](const db::Cluster::pointer& cluster) + { + valueModel->add(Wt::WString::fromUTF8(std::string{ cluster->getName() }), cluster->getId()); + }); + } + + return valueModel; + } + } void Filters::showDialog() { @@ -43,32 +101,27 @@ namespace lms::ui dialog->addFunction("id", &Wt::WTemplate::Functions::id); Wt::WComboBox* typeCombo{ dialog->bindNew("type") }; + const std::shared_ptr typeModel{ createTypeModel() }; + typeCombo->setModel(typeModel); + Wt::WComboBox* valueCombo{ dialog->bindNew("value") }; Wt::WPushButton* addBtn{ dialog->bindNew("add-btn", Wt::WString::tr("Lms.Explore.add-filter")) }; - addBtn->clicked().connect([this, typeCombo, valueCombo, dialogPtr] + addBtn->clicked().connect([this, valueCombo, dialogPtr] { - const std::string type{ typeCombo->valueText().toUTF8() }; - const std::string value{ valueCombo->valueText().toUTF8() }; - - // TODO use a model to store the cluster.id() values - ClusterId clusterId{}; + const auto valueModel{ std::static_pointer_cast(valueCombo->model()) }; + const ValueVariant value{ valueModel->getValue(valueCombo->currentIndex()) }; + if (const db::MediaLibraryId * mediaLibraryId{ std::get_if(&value) }) { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - ClusterType::pointer clusterType{ ClusterType::find(LmsApp->getDbSession(), type) }; - if (!clusterType) - return; - - Cluster::pointer cluster{ clusterType->getCluster(value) }; - if (!cluster) - return; - - clusterId = cluster->getId(); + set(*mediaLibraryId); + } + else if (const db::ClusterId * clusterId{ std::get_if(&value) }) + { + add(*clusterId); } - add(clusterId); + // TODO LmsApp->getModalManager().dispose(dialogPtr); }); @@ -78,79 +131,20 @@ namespace lms::ui LmsApp->getModalManager().dispose(dialogPtr); }); - // Populate data - { - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - const auto clusterTypesIds{ ClusterType::findUsed(LmsApp->getDbSession()) }; - for (const ClusterTypeId clusterTypeId : clusterTypesIds.results) + typeCombo->activated().connect([valueCombo, typeModel](int row) { - const auto clusterType{ ClusterType::find(LmsApp->getDbSession(), clusterTypeId) }; - typeCombo->addItem(Wt::WString::fromUTF8(std::string{ clusterType->getName() })); - } - - if (!clusterTypesIds.results.empty()) - { - const auto clusterType{ ClusterType::find(LmsApp->getDbSession(), clusterTypesIds.results.front()) }; - - for (const Cluster::pointer& cluster : clusterType->getClusters()) - { - if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); - } - } - } - - typeCombo->changed().connect([this, typeCombo, valueCombo] - { - const std::string name{ typeCombo->valueText().toUTF8() }; + const TypeVariant type{ typeModel->getValue(row) }; + const std::shared_ptr valueModel{ createValueModel(type) }; valueCombo->clear(); - - auto transaction{ LmsApp->getDbSession().createReadTransaction() }; - - auto clusterType{ ClusterType::find(LmsApp->getDbSession(), name) }; - for (const Cluster::pointer& cluster : clusterType->getClusters()) - { - if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), cluster->getId()) == _clusterIds.end()) - valueCombo->addItem(Wt::WString::fromUTF8(std::string{ cluster->getName() })); - } + valueCombo->setModel(valueModel); }); + typeCombo->activated().emit(0); // force emit to refresh the type combo model + LmsApp->getModalManager().show(std::move(dialog)); } - void Filters::add(ClusterId clusterId) - { - if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds)) - return; - - Wt::WInteractWidget* filter{}; - - { - auto cluster{ utils::createCluster(clusterId, true) }; - if (!cluster) - return; - - filter = _filters->addWidget(std::move(cluster)); - } - - _clusterIds.push_back(clusterId); - - filter->clicked().connect([this, filter, clusterId] - { - _filters->removeWidget(filter); - _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); - _sigUpdated.emit(); - }); - - LmsApp->notifyMsg(Notification::Type::Info, - Wt::WString::tr("Lms.Explore.filters"), - Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds{ 2 }); - - _sigUpdated.emit(); - } - Filters::Filters() : Wt::WTemplate{ Wt::WString::tr("Lms.Explore.template.filters") } { @@ -162,4 +156,73 @@ namespace lms::ui _filters = bindNew("clusters"); } + + void Filters::add(db::ClusterId clusterId) + { + if (std::find(std::cbegin(_clusterIds), std::cend(_clusterIds), clusterId) != std::cend(_clusterIds)) + return; + + Wt::WInteractWidget* filter{}; + + { + auto cluster{ utils::createFilterCluster(clusterId, true) }; + if (!cluster) + return; + + filter = _filters->addWidget(std::move(cluster)); + } + + _clusterIds.push_back(clusterId); + + filter->clicked().connect([this, filter, clusterId] + { + _filters->removeWidget(filter); + _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](db::ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); + _sigUpdated.emit(); + }); + + emitFilterAddedNotification(); + } + + void Filters::set(db::MediaLibraryId mediaLibraryId) + { + if (_mediaLibraryFilter) + { + _filters->removeWidget(_mediaLibraryFilter); + _mediaLibraryFilter = nullptr; + _mediaLibraryId = db::MediaLibraryId{}; + } + + std::string libraryName; + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + + const auto library{ db::MediaLibrary::find(LmsApp->getDbSession(), mediaLibraryId) }; + if (!library) + return; + + libraryName = library->getName(); + } + + _mediaLibraryId = mediaLibraryId; + _mediaLibraryFilter = _filters->addWidget(utils::createFilter(Wt::WString::fromUTF8(libraryName), Wt::WString::tr("Lms.Explore.media-library"), "bg-primary", true)); + _mediaLibraryFilter->clicked().connect(_mediaLibraryFilter, [this] + { + _filters->removeWidget(_mediaLibraryFilter); + _mediaLibraryId = db::MediaLibraryId{}; + _mediaLibraryFilter = nullptr; + _sigUpdated.emit(); + }); + + emitFilterAddedNotification(); + } + + void Filters::emitFilterAddedNotification() + { + LmsApp->notifyMsg(Notification::Type::Info, + Wt::WString::tr("Lms.Explore.filters"), + Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds{ 2 }); + + _sigUpdated.emit(); + } } // namespace lms::ui diff --git a/src/lms/ui/explore/Filters.hpp b/src/lms/ui/explore/Filters.hpp index 8db27180..f13a1d5d 100644 --- a/src/lms/ui/explore/Filters.hpp +++ b/src/lms/ui/explore/Filters.hpp @@ -19,12 +19,14 @@ #pragma once +#include #include #include #include #include #include "database/ClusterId.hpp" +#include "database/MediaLibraryId.hpp" #include "Filters.hpp" @@ -35,17 +37,24 @@ namespace lms::ui public: Filters(); - const std::vector& getClusterIds() const { return _clusterIds; } + std::span getClusters() const { return _clusterIds; } + db::MediaLibraryId getMediaLibrary() const { return _mediaLibraryId; } + void add(db::ClusterId clusterId); Wt::Signal<>& updated() { return _sigUpdated; } private: void showDialog(); + void set(db::MediaLibraryId mediaLibraryId); + void emitFilterAddedNotification(); - Wt::WContainerWidget* _filters; + Wt::WContainerWidget* _filters{}; Wt::Signal<> _sigUpdated; std::vector _clusterIds; + + Wt::WInteractWidget* _mediaLibraryFilter{}; + db::MediaLibraryId _mediaLibraryId; }; } // namespace lms::ui diff --git a/src/lms/ui/explore/PlayQueueController.cpp b/src/lms/ui/explore/PlayQueueController.cpp index 4a24d412..71606e46 100644 --- a/src/lms/ui/explore/PlayQueueController.cpp +++ b/src/lms/ui/explore/PlayQueueController.cpp @@ -28,10 +28,9 @@ namespace lms::ui { - namespace { - std::vector getArtistsTracks(db::Session& session, const std::vector& artistsId, const std::vector& clusters, std::size_t maxTrackCount) + std::vector getArtistsTracks(db::Session& session, const std::vector& artistsId, const Filters& filters, std::size_t maxTrackCount) { assert(maxTrackCount); @@ -44,7 +43,8 @@ namespace lms::ui db::Track::FindParameters params; params.setArtist(artistId); params.setSortMethod(db::TrackSortMethod::DateDescAndRelease); - params.setClusters(clusters); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setRange(db::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ db::Track::findIds(session, params) }; @@ -59,7 +59,7 @@ namespace lms::ui return res; } - std::vector getReleasesTracks(db::Session& session, const std::vector& releasesId, const std::vector& clusters, std::size_t maxTrackCount) + std::vector getReleasesTracks(db::Session& session, const std::vector& releasesId, const Filters& filters, std::size_t maxTrackCount) { using namespace db; assert(maxTrackCount); @@ -73,7 +73,8 @@ namespace lms::ui db::Track::FindParameters params; params.setRelease(releaseId); params.setSortMethod(db::TrackSortMethod::Release); - params.setClusters(clusters); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setRange(db::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ db::Track::findIds(session, params) }; @@ -88,7 +89,7 @@ namespace lms::ui return res; } - std::vector getDiscTracks(db::Session& session, const std::vector& discs, const std::vector& clusters, std::size_t maxTrackCount) + std::vector getDiscTracks(db::Session& session, const std::vector& discs, const Filters& filters, std::size_t maxTrackCount) { using namespace db; assert(maxTrackCount); @@ -103,7 +104,8 @@ namespace lms::ui params.setRelease(disc.releaseId); params.setSortMethod(db::TrackSortMethod::Release); params.setDiscNumber(disc.discNumber); - params.setClusters(clusters); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setRange(db::Range{ 0, maxTrackCount - res.size() }); const auto tracks{ db::Track::findIds(session, params) }; @@ -118,7 +120,7 @@ namespace lms::ui return res; } - std::vector getTrackListTracks(db::Session& session, db::TrackListId trackListId, const std::vector& clusters, std::size_t maxTrackCount) + std::vector getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount) { using namespace db; assert(maxTrackCount); @@ -127,7 +129,8 @@ namespace lms::ui db::Track::FindParameters params; params.setTrackList(trackListId); - params.setClusters(clusters); + params.setClusters(filters.getClusters()); + params.setMediaLibrary(filters.getMediaLibrary()); params.setRange(db::Range{ 0, maxTrackCount }); params.setSortMethod(TrackSortMethod::TrackList); @@ -143,13 +146,13 @@ namespace lms::ui void PlayQueueController::processCommand(Command command, const std::vector& artistIds) { - const std::vector tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) }; + const std::vector tracks{ getArtistsTracks(LmsApp->getDbSession(), artistIds, _filters, _maxTrackCountToEnqueue) }; processCommand(command, tracks); } void PlayQueueController::processCommand(Command command, const std::vector& releaseIds) { - const std::vector tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters.getClusterIds(), _maxTrackCountToEnqueue) }; + const std::vector tracks{ getReleasesTracks(LmsApp->getDbSession(), releaseIds, _filters, _maxTrackCountToEnqueue) }; processCommand(command, tracks); } @@ -175,13 +178,13 @@ namespace lms::ui void PlayQueueController::processCommand(Command command, db::TrackListId trackListId) { - const std::vector tracks{ getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters.getClusterIds(), _maxTrackCountToEnqueue) }; + const std::vector tracks{ getTrackListTracks(LmsApp->getDbSession(), trackListId, _filters, _maxTrackCountToEnqueue) }; processCommand(command, tracks); } void PlayQueueController::processCommand(Command command, const std::vector& discs) { - const std::vector tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters.getClusterIds(), _maxTrackCountToEnqueue) }; + const std::vector tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters, _maxTrackCountToEnqueue) }; processCommand(command, tracks); } @@ -197,7 +200,7 @@ namespace lms::ui releaseId = track->getRelease()->getId(); } - const std::vector tracks{ getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters.getClusterIds(), _maxTrackCountToEnqueue) }; + const std::vector tracks{ getReleasesTracks(LmsApp->getDbSession(), {releaseId}, _filters, _maxTrackCountToEnqueue) }; auto itTrack{ std::find(std::cbegin(tracks), std::cend(tracks), trackId) }; if (itTrack == std::cend(tracks)) return; diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index 2623f94c..98f37025 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -51,7 +51,8 @@ namespace lms::ui { feedback::IFeedbackService::FindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); releases = feedbackService.findStarredReleases(params); break; @@ -61,7 +62,8 @@ namespace lms::ui { scrobbling::IScrobblingService::FindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); releases = scrobblingService.getRecentReleases(params); @@ -72,7 +74,8 @@ namespace lms::ui { scrobbling::IScrobblingService::FindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); releases = scrobblingService.getTopReleases(params); @@ -82,7 +85,8 @@ namespace lms::ui case Mode::RecentlyAdded: { Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setSortMethod(ReleaseSortMethod::LastWritten); params.setRange(range); @@ -96,7 +100,8 @@ namespace lms::ui case Mode::Search: { Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setKeywords(getSearchKeywords()); params.setRange(range); @@ -110,7 +115,8 @@ namespace lms::ui case Mode::All: { Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setSortMethod(ReleaseSortMethod::Name); params.setRange(range); @@ -135,7 +141,8 @@ namespace lms::ui if (!_randomReleases) { Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setSortMethod(ReleaseSortMethod::Random); params.setRange(Range{ 0, getMaxCount() }); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 7a5a1e12..587b6d09 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -262,7 +262,7 @@ namespace lms::ui for (const db::Cluster::pointer& cluster : clusters) { const ClusterId clusterId{ cluster->getId() }; - Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createCluster(clusterId)) }; + Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterCluster(clusterId)) }; entry->clicked().connect([this, clusterId] { _filters.add(clusterId); @@ -394,7 +394,8 @@ namespace lms::ui db::Track::FindParameters params; params.setRelease(_releaseId); params.setSortMethod(db::TrackSortMethod::Release); - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) { diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index 4af3853c..b54d5a5a 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -53,7 +53,8 @@ namespace lms::ui case Mode::Starred: { feedback::IFeedbackService::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); params.setUser(LmsApp->getUserId()); tracks = feedbackService.findStarredTracks(params); @@ -64,7 +65,8 @@ namespace lms::ui { scrobbling::IScrobblingService::FindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); tracks = scrobblingService.getRecentTracks(params); @@ -75,7 +77,8 @@ namespace lms::ui { scrobbling::IScrobblingService::FindParameters params; params.setUser(LmsApp->getUserId()); - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); tracks = scrobblingService.getTopTracks(params); @@ -85,7 +88,8 @@ namespace lms::ui case Mode::RecentlyAdded: { Track::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setSortMethod(TrackSortMethod::LastWritten); params.setRange(range); @@ -99,7 +103,8 @@ namespace lms::ui case Mode::Search: { Track::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setKeywords(getSearchKeywords()); params.setRange(range); @@ -113,7 +118,8 @@ namespace lms::ui case Mode::All: { Track::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setRange(range); { @@ -137,7 +143,8 @@ namespace lms::ui if (!_randomTracks) { Track::FindParameters params; - params.setClusters(getFilters().getClusterIds()); + params.setClusters(getFilters().getClusters()); + params.setMediaLibrary(getFilters().getMediaLibrary()); params.setSortMethod(TrackSortMethod::Random); params.setRange(Range{ 0, getMaxCount() }); diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index b2496405..24fe180f 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -138,7 +138,7 @@ namespace lms::ui::TrackListHelpers trackInfo->bindInt("playcount", core::Service::get()->getCount(LmsApp->getUserId(), track->getId())); - Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createClustersForTrack(track, filters)) }; + Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createFilterClustersForTrack(track, filters)) }; if (clusterContainer->count() > 0) trackInfo->setCondition("if-has-clusters", true); diff --git a/src/lms/ui/explore/TrackListView.cpp b/src/lms/ui/explore/TrackListView.cpp index 4fd379d3..57220e95 100644 --- a/src/lms/ui/explore/TrackListView.cpp +++ b/src/lms/ui/explore/TrackListView.cpp @@ -107,7 +107,7 @@ namespace lms::ui for (const db::Cluster::pointer& cluster : clusters) { const ClusterId clusterId{ cluster->getId() }; - Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createCluster(clusterId)) }; + Wt::WInteractWidget* entry{ clusterContainers->addWidget(utils::createFilterCluster(clusterId)) }; entry->clicked().connect([this, clusterId] { _filters.add(clusterId); @@ -183,7 +183,8 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createReadTransaction() }; db::Track::FindParameters params; - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); params.setTrackList(_trackListId); params.setSortMethod(db::TrackSortMethod::TrackList); params.setRange(db::Range{ static_cast(_container->getCount()), _batchSize }); diff --git a/src/lms/ui/explore/TrackListsView.cpp b/src/lms/ui/explore/TrackListsView.cpp index fb023f35..4d903964 100644 --- a/src/lms/ui/explore/TrackListsView.cpp +++ b/src/lms/ui/explore/TrackListsView.cpp @@ -101,7 +101,8 @@ namespace lms::ui auto transaction{ session.createReadTransaction() }; TrackList::FindParameters params; - params.setClusters(_filters.getClusterIds()); + params.setClusters(_filters.getClusters()); + params.setMediaLibrary(_filters.getMediaLibrary()); params.setUser(LmsApp->getUserId()); params.setType(TrackListType::Playlist); params.setRange(range);