From 0cf6b2159e5fa6b5e087d11f5c4f02cb65dfbe29 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 4 Dec 2021 15:25:09 +0100 Subject: [PATCH] Databsae service refactoring. Warning, loses stars and listens stats --- src/libs/services/CMakeLists.txt | 1 - .../services/auth/impl/AuthServiceBase.cpp | 4 +- .../services/auth/impl/AuthServiceBase.hpp | 2 +- .../services/auth/impl/AuthTokenService.cpp | 7 +- .../impl/internal/InternalPasswordService.cpp | 4 +- .../services/auth/IAuthTokenService.hpp | 2 +- .../include/services/auth/IEnvService.hpp | 2 +- .../services/auth/IPasswordService.hpp | 2 +- src/libs/services/cover/impl/CoverService.cpp | 4 +- .../include/services/cover/ICoverService.hpp | 3 +- src/libs/services/database/CMakeLists.txt | 6 + src/libs/services/database/impl/Artist.cpp | 367 ++----- src/libs/services/database/impl/AuthToken.cpp | 67 ++ src/libs/services/database/impl/Cluster.cpp | 104 +- .../impl/{Traits.hpp => IdTypeTraits.hpp} | 0 src/libs/services/database/impl/Listen.cpp | 285 ++++++ src/libs/services/database/impl/Release.cpp | 291 ++---- .../services/database/impl/ScanSettings.cpp | 2 +- src/libs/services/database/impl/Session.cpp | 71 +- .../services/database/impl/StarredArtist.cpp | 79 ++ .../services/database/impl/StarredRelease.cpp | 79 ++ .../services/database/impl/StarredTrack.cpp | 79 ++ src/libs/services/database/impl/Track.cpp | 355 ++----- .../database/impl/TrackArtistLink.cpp | 4 +- .../services/database/impl/TrackBookmark.cpp | 41 +- .../services/database/impl/TrackFeatures.cpp | 55 +- src/libs/services/database/impl/TrackList.cpp | 190 +++- src/libs/services/database/impl/Types.cpp | 52 + src/libs/services/database/impl/User.cpp | 167 +--- src/libs/services/database/impl/Utils.cpp | 7 + src/libs/services/database/impl/Utils.hpp | 57 +- .../include/services/database/Artist.hpp | 79 +- .../include/services/database/ArtistId.hpp | 25 + .../include/services/database/AuthToken.hpp | 66 ++ .../include/services/database/AuthTokenId.hpp | 24 + .../include/services/database/Cluster.hpp | 38 +- .../include/services/database/ClusterId.hpp | 26 + .../include/services/database/IdType.hpp | 72 ++ .../include/services/database/Listen.hpp | 111 +++ .../include/services/database/Object.hpp | 67 ++ .../include/services/database/Release.hpp | 62 +- .../include/services/database/ReleaseId.hpp | 25 + .../services/database/ScanSettings.hpp | 5 +- .../services/database/StarredArtist.hpp | 81 ++ .../services/database/StarredRelease.hpp | 81 ++ .../services/database/StarredTrack.hpp | 81 ++ .../include/services/database/Track.hpp | 82 +- .../services/database/TrackArtistLink.hpp | 9 +- .../services/database/TrackBookmark.hpp | 20 +- .../services/database/TrackFeatures.hpp | 17 +- .../include/services/database/TrackId.hpp | 25 + .../include/services/database/TrackList.hpp | 54 +- .../include/services/database/TrackListId.hpp | 26 + .../include/services/database/Types.hpp | 218 ++--- .../include/services/database/User.hpp | 117 +-- .../include/services/database/UserId.hpp | 25 + src/libs/services/database/test/Artist.cpp | 184 ++-- .../services/database/test/CMakeLists.txt | 8 + src/libs/services/database/test/Cluster.cpp | 371 ++++---- src/libs/services/database/test/Common.cpp | 211 +++++ src/libs/services/database/test/Common.hpp | 60 +- .../services/database/test/DatabaseTest.cpp | 218 +---- src/libs/services/database/test/Listen.cpp | 892 ++++++++++++++++++ src/libs/services/database/test/Release.cpp | 133 +-- .../services/database/test/StarredArtist.cpp | 109 +++ .../services/database/test/StarredRelease.cpp | 109 +++ .../services/database/test/StarredTrack.cpp | 109 +++ src/libs/services/database/test/Track.cpp | 71 +- .../services/database/test/TrackBookmark.cpp | 66 ++ .../services/database/test/TrackFeatures.cpp | 48 + src/libs/services/database/test/TrackList.cpp | 366 +++++++ src/libs/services/feedback/CMakeLists.txt | 19 - .../feedback/impl/FeedbackService.cpp | 183 ---- .../feedback/impl/FeedbackService.hpp | 59 -- .../services/feedback/IFeedbackService.hpp | 51 - .../services/recommendation/impl/IEngine.hpp | 9 +- .../impl/RecommendationService.cpp | 10 +- .../impl/RecommendationService.hpp | 4 +- .../impl/clusters/ClustersEngine.cpp | 55 +- .../impl/clusters/ClustersEngine.hpp | 4 +- .../impl/features/FeaturesEngine.cpp | 118 +-- .../impl/features/FeaturesEngine.hpp | 11 +- .../impl/features/FeaturesEngineCache.hpp | 3 +- .../recommendation/IRecommendationService.hpp | 7 +- .../include/services/recommendation/Types.hpp | 4 +- .../services/scanner/impl/ScannerService.cpp | 85 +- .../include/services/scanner/ScannerStats.hpp | 2 +- .../services/scrobbling/impl/IScrobbler.hpp | 19 +- .../scrobbling/impl/ScrobblerBase.cpp | 72 ++ .../scrobbling/impl/ScrobblingService.cpp | 272 +++--- .../scrobbling/impl/ScrobblingService.hpp | 44 +- .../impl/ScrobblingService.impl.hpp | 93 ++ .../impl/internal/InternalScrobbler.cpp | 34 +- .../impl/internal/InternalScrobbler.hpp | 6 +- .../listenbrainz/ListenBrainzScrobbler.cpp | 10 +- .../listenbrainz/ListenBrainzScrobbler.hpp | 2 - .../impl/listenbrainz/ListensSynchronizer.cpp | 70 +- .../impl/listenbrainz/ListensSynchronizer.hpp | 1 - .../impl/listenbrainz/LovesSynchronizer.hpp | 92 ++ .../scrobbling/impl/listenbrainz/Utils.cpp | 19 +- .../scrobbling/impl/listenbrainz/Utils.hpp | 5 +- .../scrobbling/IScrobblingService.hpp | 59 +- .../include/services/scrobbling/Listen.hpp | 3 +- src/libs/subsonic/CMakeLists.txt | 1 - src/libs/subsonic/impl/RequestContext.hpp | 2 +- src/libs/subsonic/impl/Stream.cpp | 6 +- src/libs/subsonic/impl/SubsonicId.hpp | 5 +- src/libs/subsonic/impl/SubsonicResource.cpp | 382 ++++---- src/lms/CMakeLists.txt | 1 - src/lms/main.cpp | 2 - src/lms/ui/Auth.cpp | 4 +- src/lms/ui/Auth.hpp | 3 +- src/lms/ui/LmsApplication.cpp | 4 +- src/lms/ui/LmsApplication.hpp | 3 + src/lms/ui/LmsApplicationManager.hpp | 2 +- src/lms/ui/LmsTheme.cpp | 10 +- src/lms/ui/LmsTheme.hpp | 10 +- src/lms/ui/MediaPlayer.cpp | 8 +- src/lms/ui/MediaPlayer.hpp | 1 + src/lms/ui/PlayQueue.cpp | 22 +- src/lms/ui/PlayQueue.hpp | 3 +- src/lms/ui/PlayQueueAction.hpp | 4 +- src/lms/ui/SettingsView.cpp | 19 +- src/lms/ui/admin/InitWizardView.cpp | 2 +- src/lms/ui/admin/ScannerController.cpp | 2 +- src/lms/ui/admin/UserView.cpp | 28 +- src/lms/ui/admin/UsersView.cpp | 8 +- src/lms/ui/explore/ArtistCollector.cpp | 130 ++- src/lms/ui/explore/ArtistCollector.hpp | 10 +- src/lms/ui/explore/ArtistListHelpers.hpp | 2 +- src/lms/ui/explore/ArtistView.cpp | 55 +- src/lms/ui/explore/ArtistView.hpp | 2 +- src/lms/ui/explore/ArtistsView.cpp | 22 +- src/lms/ui/explore/DatabaseCollectorBase.cpp | 17 +- src/lms/ui/explore/DatabaseCollectorBase.hpp | 11 +- src/lms/ui/explore/Explore.cpp | 4 +- src/lms/ui/explore/Filters.cpp | 35 +- src/lms/ui/explore/Filters.hpp | 2 +- src/lms/ui/explore/ReleaseCollector.cpp | 123 ++- src/lms/ui/explore/ReleaseCollector.hpp | 13 +- src/lms/ui/explore/ReleaseListHelpers.hpp | 2 +- src/lms/ui/explore/ReleasePopup.cpp | 8 +- src/lms/ui/explore/ReleaseView.cpp | 20 +- src/lms/ui/explore/ReleaseView.hpp | 2 +- src/lms/ui/explore/ReleasesView.cpp | 20 +- src/lms/ui/explore/SearchView.cpp | 53 +- src/lms/ui/explore/TrackCollector.cpp | 121 ++- src/lms/ui/explore/TrackCollector.hpp | 11 +- src/lms/ui/explore/TrackListHelpers.hpp | 2 +- src/lms/ui/explore/TrackPopup.cpp | 10 +- src/lms/ui/explore/TracksView.cpp | 20 +- src/lms/ui/resource/AudioFileResource.cpp | 2 +- src/lms/ui/resource/AudioFileResource.hpp | 2 +- .../ui/resource/AudioTranscodeResource.cpp | 4 +- .../ui/resource/AudioTranscodeResource.hpp | 2 +- src/lms/ui/resource/CoverResource.hpp | 3 +- src/lms/ui/resource/DownloadResource.cpp | 12 +- src/lms/ui/resource/DownloadResource.hpp | 4 +- src/tools/cover/LmsCover.cpp | 8 +- .../recommendation/LmsRecommendation.cpp | 55 +- 160 files changed, 6047 insertions(+), 3477 deletions(-) create mode 100644 src/libs/services/database/impl/AuthToken.cpp rename src/libs/services/database/impl/{Traits.hpp => IdTypeTraits.hpp} (100%) create mode 100644 src/libs/services/database/impl/Listen.cpp create mode 100644 src/libs/services/database/impl/StarredArtist.cpp create mode 100644 src/libs/services/database/impl/StarredRelease.cpp create mode 100644 src/libs/services/database/impl/StarredTrack.cpp create mode 100644 src/libs/services/database/impl/Types.cpp create mode 100644 src/libs/services/database/include/services/database/ArtistId.hpp create mode 100644 src/libs/services/database/include/services/database/AuthToken.hpp create mode 100644 src/libs/services/database/include/services/database/AuthTokenId.hpp create mode 100644 src/libs/services/database/include/services/database/ClusterId.hpp create mode 100644 src/libs/services/database/include/services/database/IdType.hpp create mode 100644 src/libs/services/database/include/services/database/Listen.hpp create mode 100644 src/libs/services/database/include/services/database/Object.hpp create mode 100644 src/libs/services/database/include/services/database/ReleaseId.hpp create mode 100644 src/libs/services/database/include/services/database/StarredArtist.hpp create mode 100644 src/libs/services/database/include/services/database/StarredRelease.hpp create mode 100644 src/libs/services/database/include/services/database/StarredTrack.hpp create mode 100644 src/libs/services/database/include/services/database/TrackId.hpp create mode 100644 src/libs/services/database/include/services/database/TrackListId.hpp create mode 100644 src/libs/services/database/include/services/database/UserId.hpp create mode 100644 src/libs/services/database/test/Common.cpp create mode 100644 src/libs/services/database/test/Listen.cpp create mode 100644 src/libs/services/database/test/StarredArtist.cpp create mode 100644 src/libs/services/database/test/StarredRelease.cpp create mode 100644 src/libs/services/database/test/StarredTrack.cpp create mode 100644 src/libs/services/database/test/TrackBookmark.cpp create mode 100644 src/libs/services/database/test/TrackFeatures.cpp create mode 100644 src/libs/services/database/test/TrackList.cpp delete mode 100644 src/libs/services/feedback/CMakeLists.txt delete mode 100644 src/libs/services/feedback/impl/FeedbackService.cpp delete mode 100644 src/libs/services/feedback/impl/FeedbackService.hpp delete mode 100644 src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp create mode 100644 src/libs/services/scrobbling/impl/ScrobblerBase.cpp create mode 100644 src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp create mode 100644 src/libs/services/scrobbling/impl/listenbrainz/LovesSynchronizer.hpp diff --git a/src/libs/services/CMakeLists.txt b/src/libs/services/CMakeLists.txt index 70f360ed..fd4d3160 100644 --- a/src/libs/services/CMakeLists.txt +++ b/src/libs/services/CMakeLists.txt @@ -1,7 +1,6 @@ add_subdirectory(auth) add_subdirectory(cover) add_subdirectory(database) -add_subdirectory(feedback) add_subdirectory(recommendation) add_subdirectory(scanner) add_subdirectory(scrobbling) diff --git a/src/libs/services/auth/impl/AuthServiceBase.cpp b/src/libs/services/auth/impl/AuthServiceBase.cpp index 6b3c9a95..01b87a0a 100644 --- a/src/libs/services/auth/impl/AuthServiceBase.cpp +++ b/src/libs/services/auth/impl/AuthServiceBase.cpp @@ -38,7 +38,7 @@ namespace Auth Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(session, loginName)}; + User::pointer user {User::find(session, loginName)}; if (!user) { const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR}; @@ -58,7 +58,7 @@ namespace Auth Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - User::pointer user {User::getById(session, userId)}; + User::pointer user {User::find(session, userId)}; if (user) user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); } diff --git a/src/libs/services/auth/impl/AuthServiceBase.hpp b/src/libs/services/auth/impl/AuthServiceBase.hpp index 812e716f..50879896 100644 --- a/src/libs/services/auth/impl/AuthServiceBase.hpp +++ b/src/libs/services/auth/impl/AuthServiceBase.hpp @@ -20,7 +20,7 @@ #pragma once #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { diff --git a/src/libs/services/auth/impl/AuthTokenService.cpp b/src/libs/services/auth/impl/AuthTokenService.cpp index 24b9cee4..01ac9c87 100644 --- a/src/libs/services/auth/impl/AuthTokenService.cpp +++ b/src/libs/services/auth/impl/AuthTokenService.cpp @@ -24,6 +24,7 @@ #include #include "services/auth/Types.hpp" +#include "services/database/AuthToken.hpp" #include "services/database/Session.hpp" #include "services/database/User.hpp" #include "utils/Exception.hpp" @@ -55,7 +56,7 @@ namespace Auth auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User deleted"}; @@ -77,7 +78,7 @@ namespace Auth Database::Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)}; + Database::AuthToken::pointer authToken {Database::AuthToken::find(session, secretHash)}; if (!authToken) return std::nullopt; @@ -132,7 +133,7 @@ namespace Auth auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User deleted"}; diff --git a/src/libs/services/auth/impl/internal/InternalPasswordService.cpp b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp index d63de7ed..ebc55158 100644 --- a/src/libs/services/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp @@ -51,7 +51,7 @@ namespace Auth Database::Session& session {getDbSession()}; auto transaction {session.createSharedTransaction()}; - const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + const Database::User::pointer user {Database::User::find(session, loginName)}; if (!user) { LMS_LOG(AUTH, DEBUG) << "hashing random stuff"; @@ -102,7 +102,7 @@ namespace Auth Database::Session& session {getDbSession()}; auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, userId)}; + Database::User::pointer user {Database::User::find(session, userId)}; if (!user) throw Exception {"User not found!"}; diff --git a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp index 884dd7d1..bd5754de 100644 --- a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp +++ b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp @@ -27,7 +27,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { diff --git a/src/libs/services/auth/include/services/auth/IEnvService.hpp b/src/libs/services/auth/include/services/auth/IEnvService.hpp index 291bfab6..ad9dda2c 100644 --- a/src/libs/services/auth/include/services/auth/IEnvService.hpp +++ b/src/libs/services/auth/include/services/auth/IEnvService.hpp @@ -22,7 +22,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { diff --git a/src/libs/services/auth/include/services/auth/IPasswordService.hpp b/src/libs/services/auth/include/services/auth/IPasswordService.hpp index 4958e746..18319106 100644 --- a/src/libs/services/auth/include/services/auth/IPasswordService.hpp +++ b/src/libs/services/auth/include/services/auth/IPasswordService.hpp @@ -26,7 +26,7 @@ #include #include "services/auth/Types.hpp" -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { diff --git a/src/libs/services/cover/impl/CoverService.cpp b/src/libs/services/cover/impl/CoverService.cpp index 58537ad1..a9c2b8e4 100644 --- a/src/libs/services/cover/impl/CoverService.cpp +++ b/src/libs/services/cover/impl/CoverService.cpp @@ -50,7 +50,7 @@ namespace auto transaction {dbSession.createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(dbSession, trackId)}; + const Database::Track::pointer track {Database::Track::find(dbSession, trackId)}; if (!track) return res; @@ -374,7 +374,7 @@ CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width) auto transaction {session.createSharedTransaction()}; - if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)}) + if (const Database::Release::pointer release {Database::Release::find(session, releaseId)}) { if (const auto firstTrack {release->getFirstTrack()}) { diff --git a/src/libs/services/cover/include/services/cover/ICoverService.hpp b/src/libs/services/cover/include/services/cover/ICoverService.hpp index 392f0dfc..0421f589 100644 --- a/src/libs/services/cover/include/services/cover/ICoverService.hpp +++ b/src/libs/services/cover/include/services/cover/ICoverService.hpp @@ -22,7 +22,8 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" #include "image/IEncodedImage.hpp" namespace Database diff --git a/src/libs/services/database/CMakeLists.txt b/src/libs/services/database/CMakeLists.txt index 38b13e75..8e8b9fe3 100644 --- a/src/libs/services/database/CMakeLists.txt +++ b/src/libs/services/database/CMakeLists.txt @@ -1,16 +1,22 @@ add_library(lmsdatabase SHARED impl/Artist.cpp + impl/AuthToken.cpp impl/Cluster.cpp impl/Db.cpp + impl/Listen.cpp impl/TrackArtistLink.cpp impl/TrackFeatures.cpp impl/TrackList.cpp impl/Release.cpp impl/ScanSettings.cpp impl/Session.cpp + impl/StarredArtist.cpp + impl/StarredRelease.cpp + impl/StarredTrack.cpp impl/SqlQuery.cpp impl/Track.cpp impl/TrackBookmark.cpp + impl/Types.cpp impl/User.cpp impl/Utils.cpp ) diff --git a/src/libs/services/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index 9b139381..6a7428a0 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -28,7 +28,7 @@ #include "utils/Logger.hpp" #include "SqlQuery.hpp" #include "Utils.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" namespace Database { @@ -40,8 +40,16 @@ _MBID {MBID ? MBID->getAsString() : ""} { } +std::size_t +Artist::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM artist"); +} + std::vector -Artist::getByName(Session& session, const std::string& name) +Artist::find(Session& session, const std::string& name) { session.checkSharedLocked(); @@ -53,14 +61,14 @@ Artist::getByName(Session& session, const std::string& name) } Artist::pointer -Artist::getByMBID(Session& session, const UUID& mbid) +Artist::find(Session& session, const UUID& mbid) { session.checkSharedLocked(); return session.getDboSession().find().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue(); } Artist::pointer -Artist::getById(Session& session, ArtistId id) +Artist::find(Session& session, ArtistId id) { session.checkSharedLocked(); return session.getDboSession().find().where("id = ?").bind(id).resultValue(); @@ -84,36 +92,37 @@ Artist::create(Session& session, const std::string& name, const std::optional static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional linkType) +Wt::Dbo::Query +createQuery(Session& session, const Artist::FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().query(queryStr)}; - query.join("track t ON t.id = t_a_l.track_id"); - query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); + auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a")}; + if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType) + { + query.join("track t ON t.id = t_a_l.track_id"); + query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); + } - if (linkType) - query.where("t_a_l.type = ?").bind(*linkType); + if (params.linkType) + query.where("t_a_l.type = ?").bind(*params.linkType); - if (!keywords.empty()) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (!params.keywords.empty()) { std::vector clauses; std::vector sortClauses; - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) { clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + escapeLikeKeyword(keyword) + "%"); } - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) { sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); query.bind("%" + escapeLikeKeyword(keyword) + "%"); @@ -122,7 +131,15 @@ createQuery(Session& session, query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")"); } - if (!clusterIds.empty()) + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_artist s_a ON s_a.artist_id = a.id") + .where("s_a.user_id = ?").bind(params.starringUser) + .where("s_a.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" @@ -132,268 +149,59 @@ createQuery(Session& session, " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case ArtistSortMethod::None: + break; + case ArtistSortMethod::ByName: + query.orderBy("a.name COLLATE NOCASE"); + break; + case ArtistSortMethod::BySortName: + query.orderBy("a.sort_name COLLATE NOCASE"); + break; + case ArtistSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ArtistSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case ArtistSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_a.date_time DESC"); + break; + } + return query; } -std::vector -Artist::getAll(Session& session) +RangeResults +Artist::findAllOrphans(Session& session, Range range) { session.checkSharedLocked(); + auto query {session.getDboSession().query("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")}; - Wt::Dbo::collection> res = session.getDboSession().find(); - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Artist::getAll(Session& session, SortMethod sortMethod) +RangeResults +Artist::find(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().find()}; - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> res = query; - return std::vector(res.begin(), res.end()); -} - - -std::vector -Artist::getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)}; - - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Artist::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM artist"); - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size) -{ - session.checkSharedLocked(); - - auto query {createQuery(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)}; - - Wt::Dbo::collection res = query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1); - - return std::vector(res.begin(), res.end()); - -} - -std::vector -Artist::getAllOrphans(Session& session) -{ - session.checkSharedLocked(); - Wt::Dbo::collection> res {session.getDboSession().query>("SELECT DISTINCT a FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT a.id FROM artist a" - " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getByClusters(Session& session, const std::vector& clusters, SortMethod sortMethod) -{ - assert(!clusters.empty()); - - session.checkSharedLocked(); - bool more{}; - return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more); -} - -std::vector -Artist::getByFilter(Session& session, - const std::vector& clusters, - const std::vector& keywords, - std::optional linkType, - SortMethod sortMethod, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)}; - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("a.name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("a.sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Artist::getLastWritten(Session& session, - std::optional after, - const std::vector& clusters, - std::optional linkType, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; - - if (after) - query.where("t.file_last_write > ?").bind(*after); - - Wt::Dbo::collection> collection = query - .orderBy("t.file_last_write DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Artist::getStarred(Session& session, - User::pointer user, - const std::vector& clusters, - std::optional linkType, - SortMethod sortMethod, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)}; - - { - std::ostringstream oss; - oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" - " INNER JOIN user_artist_starred uas ON uas.artist_id = a.id" - " INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)"; - - query.bind(user->getId()); - query.where(oss.str()); - } - - switch (sortMethod) - { - case Artist::SortMethod::None: - break; - case Artist::SortMethod::ByName: - query.orderBy("name COLLATE NOCASE"); - break; - case Artist::SortMethod::BySortName: - query.orderBy("sort_name COLLATE NOCASE"); - break; - } - - Wt::Dbo::collection> collection = query - .groupBy("a.id") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1); - - std::vector res (collection.begin(), collection.end()); - - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return std::vector(res.begin(), res.end()); + auto query {createQuery(session, params)}; + return execQuery(query, params.range); } std::vector @@ -463,32 +271,19 @@ Artist::getTracks(std::optional linkType) const return std::vector(tracks.begin(), tracks.end()); } -std::vector -Artist::getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const +RangeResults +Artist::getNonReleaseTracks(std::optional linkType, Range range) const { assert(session()); auto query {session()->query>("SELECT t FROM track t INNER JOIN artist a ON a.id = t_a_l.artist_id INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id") .where("a.id = ?").bind(getId()) .where("t.release_id is NULL") - .orderBy("t.name") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1)}; - + .orderBy("t.name")}; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); - Wt::Dbo::collection> tracks {query.resultList()}; - std::vector res(tracks.begin(), tracks.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, range); } bool @@ -518,14 +313,14 @@ Artist::getRandomTracks(std::optional count) const return std::vector(tracks.begin(), tracks.end()); } -std::vector -Artist::getSimilarArtists(EnumSet artistLinkTypes, std::optional range) const +RangeResults +Artist::findSimilarArtists(EnumSet artistLinkTypes, Range range) const { assert(session()); std::ostringstream oss; oss << - "SELECT a FROM artist a" + "SELECT a.id FROM artist a" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" @@ -554,19 +349,15 @@ Artist::getSimilarArtists(EnumSet artistLinkTypes, std::opt oss << ")"; } - Wt::Dbo::Query> query {session()->query>(oss.str()) + auto query {session()->query(oss.str()) .bind(getId()) .bind(getId()) .groupBy("a.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(range ? static_cast(range->limit) : -1) - .offset(range ? static_cast(range->offset) : -1)}; - + .orderBy("COUNT(*) DESC, RANDOM()")}; for (TrackArtistLinkType type : artistLinkTypes) query.bind(type); - Wt::Dbo::collection> res {query.resultList()}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } std::vector> diff --git a/src/libs/services/database/impl/AuthToken.cpp b/src/libs/services/database/impl/AuthToken.cpp new file mode 100644 index 00000000..52efdace --- /dev/null +++ b/src/libs/services/database/impl/AuthToken.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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 "services/database/AuthToken.hpp" + +#include +#include "services/database/Session.hpp" +#include "services/database/User.hpp" +#include "StringViewTraits.hpp" +#include "IdTypeTraits.hpp" + +namespace Database +{ + + AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + : _value {value} + , _expiry {expiry} + , _user {getDboPtr(user)} + { + + } + + AuthToken::pointer + AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) + { + session.checkUniqueLocked(); + + AuthToken::pointer res {session.getDboSession().add(std::make_unique(value, expiry, user))}; + session.getDboSession().flush(); + + return res; + } + + void + AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) + { + session.checkUniqueLocked(); + + session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now); + } + + AuthToken::pointer + AuthToken::find(Session& session, std::string_view value) + { + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("value = ?").bind(value) + .resultValue(); + } +} diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp index cf4350cc..87ec28b1 100644 --- a/src/libs/services/database/impl/Cluster.cpp +++ b/src/libs/services/database/impl/Cluster.cpp @@ -24,8 +24,9 @@ #include "services/database/ScanSettings.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" +#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" -#include "Traits.hpp" +#include "Utils.hpp" namespace Database { @@ -35,6 +36,14 @@ Cluster::Cluster(ObjectPtr type, std::string_view name) { } +std::size_t +Cluster::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM cluster"); +} + Cluster::pointer Cluster::create(Session& session, ObjectPtr type, std::string_view name) { @@ -46,25 +55,26 @@ Cluster::create(Session& session, ObjectPtr type, std::string_view return res; } -std::vector -Cluster::getAll(Session& session) +RangeResults +Cluster::find(Session& session, Range range) { session.checkSharedLocked(); + auto query {session.getDboSession().query("SELECT id FROM cluster")}; - Wt::Dbo::collection> res {session.getDboSession().find()}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Cluster::getAllOrphans(Session& session) +RangeResults +Cluster::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().query>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)").resultList()}; - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")}; + + return execQuery(query, range); } Cluster::pointer -Cluster::getById(Session& session, ClusterId id) +Cluster::find(Session& session, ClusterId id) { session.checkSharedLocked(); @@ -77,29 +87,15 @@ Cluster::addTrack(ObjectPtr track) _tracks.insert(getDboPtr(track)); } -std::vector -Cluster::getTracks(std::optional offset, std::optional limit) const +RangeResults +Cluster::getTracks(Range range) const { assert(session()); - auto res {session()->query>("SELECT t FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .where("c.id = ?").bind(getId()) - .offset(offset ? static_cast(*offset) : -1) - .limit(limit ? static_cast(*limit) : -1) - .resultList()}; + auto query {session()->query("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") + .where("c.id = ?").bind(getId())}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Cluster::getTrackIds() const -{ - assert(session()); - - Wt::Dbo::collection res = session()->query("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id") - .where("c.id = ?").bind(getId()); - - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } std::size_t @@ -117,33 +113,42 @@ ClusterType::ClusterType(std::string_view name) { } -std::vector -ClusterType::getAllOrphans(Session& session) +std::size_t +ClusterType::getCount(Session& session) { session.checkSharedLocked(); - Wt::Dbo::collection> res = session.getDboSession().query>( - "SELECT c_t from cluster_type c_t" - " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") - .where("c.id IS NULL"); - - return std::vector(res.begin(), res.end()); + return session.getDboSession().query("SELECT COUNT(*) FROM cluster_type"); } -std::vector -ClusterType::getAllUsed(Session& session) + +RangeResults +ClusterType::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - Wt::Dbo::collection> res = session.getDboSession().query>( - "SELECT DISTINCT c_t from cluster_type c_t") - .join("cluster c ON c_t.id = c.cluster_type_id"); + auto query {session.getDboSession().query( + "SELECT c_t.id from cluster_type c_t" + " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") + .where("c.id IS NULL")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); +} + +RangeResults +ClusterType::findUsed(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query( + "SELECT DISTINCT c_t.id from cluster_type c_t") + .join("cluster c ON c_t.id = c.cluster_type_id")}; + + return execQuery(query, range); } ClusterType::pointer -ClusterType::getByName(Session& session, const std::string& name) +ClusterType::find(Session& session, const std::string& name) { session.checkSharedLocked(); @@ -151,20 +156,21 @@ ClusterType::getByName(Session& session, const std::string& name) } ClusterType::pointer -ClusterType::getById(Session& session, ClusterTypeId id) +ClusterType::find(Session& session, ClusterTypeId id) { session.checkSharedLocked(); return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } -std::vector -ClusterType::getAll(Session& session) +RangeResults +ClusterType::find(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find().resultList()}; - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT id from cluster_type")}; + + return execQuery(query, range); } ClusterType::pointer diff --git a/src/libs/services/database/impl/Traits.hpp b/src/libs/services/database/impl/IdTypeTraits.hpp similarity index 100% rename from src/libs/services/database/impl/Traits.hpp rename to src/libs/services/database/impl/IdTypeTraits.hpp diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp new file mode 100644 index 00000000..fde20d8c --- /dev/null +++ b/src/libs/services/database/impl/Listen.cpp @@ -0,0 +1,285 @@ +/* + * 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 "services/database/Listen.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "SqlQuery.hpp" +#include "Utils.hpp" + +namespace +{ + using namespace Database; + + Wt::Dbo::Query + createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType) + { + auto query {session.query("SELECT a.id from artist a") + .join("track t ON t.id = t_a_l.track_id") + .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (linkType) + query.where("t_a_l.type = ?").bind(*linkType); + + if (!clusterIds.empty()) + { + std::ostringstream oss; + oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" + " INNER JOIN track t ON t.id = t_a_l.track_id" + " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + + WhereClause clusterClause; + for (auto id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } + + Wt::Dbo::Query + createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + { + auto query {session.query("SELECT r.id from release r") + .join("track t ON t.release_id = r.id") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (!clusterIds.empty()) + { + std::ostringstream oss; + oss << "r.id IN (SELECT DISTINCT r.id FROM release r" + " INNER JOIN track t ON t.release_id = r.id" + " INNER JOIN cluster c ON c.id = t_c.cluster_id" + " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + + WhereClause clusterClause; + for (ClusterId id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } + + Wt::Dbo::Query + createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + { + auto query {session.query("SELECT t.id from track t") + .join("listen l ON l.track_id = t.id") + .where("l.user_id = ?").bind(userId) + .where("l.scrobbler = ?").bind(scrobbler)}; + + if (!clusterIds.empty()) + { + 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 (auto id : clusterIds) + { + clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); + query.bind(id); + } + + oss << " " << clusterClause.get(); + oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + + query.where(oss.str()); + } + + return query; + } +} + +namespace Database +{ + Listen::Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + : _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} + , _scrobbler {scrobbler} + , _user {getDboPtr(user)} + , _track {getDboPtr(track)} + {} + + std::size_t + Listen::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM listen"); + } + + Listen::pointer + Listen::find(Session& session, ListenId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + RangeResults + Listen::find(Session& session, UserId userId, Scrobbler scrobbler, Range range) + { + session.checkSharedLocked(); + + auto query {session.getDboSession().find() + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .orderBy("date_time")}; + + return execQuery(query, range); + } + + Listen::pointer + Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + { + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) + .where("scrobbler = ?").bind(scrobbler) + .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) + .resultValue(); + } + + Listen::pointer + Listen::create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + { + session.checkUniqueLocked(); + + Listen::pointer res {session.getDboSession().add(std::make_unique(user, track, scrobbler, dateTime))}; + session.getDboSession().flush(); + + return res; + } + + RangeResults + Listen::getTopArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range) + { + auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)}; + + auto collection {query + .orderBy("COUNT(a.id) DESC") + .groupBy("a.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getTopReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .orderBy("COUNT(r.id) DESC") + .groupBy("r.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getTopTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .orderBy("COUNT(t.id) DESC") + .groupBy("t.id")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range) + { + auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) + .groupBy("a.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .groupBy("r.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + RangeResults + Listen::getRecentTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range) + { + auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + .groupBy("t.id").having("l.date_time = MAX(l.date_time)") + .orderBy("l.date_time DESC")}; + + return execQuery(query, range); + } + + +} // namespace Database + diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index a25d23db..c268ef7c 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -28,28 +28,45 @@ #include "services/database/User.hpp" #include "utils/Logger.hpp" #include "SqlQuery.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" #include "Utils.hpp" namespace Database { -template -static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords) +Wt::Dbo::Query +createQuery(Session& session, const Release::FindParameters& params) { + auto query {session.getDboSession().query("SELECT DISTINCT r.id from release r")}; - auto query {session.getDboSession().query(queryStr)}; - query.join("track t ON t.release_id = r.id"); + if (params.sortMethod == ReleaseSortMethod::LastWritten + || params.writtenAfter.isValid() + || params.dateRange) + { + query.join("track t ON t.release_id = r.id"); + } - for (std::string_view keyword : keywords) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.dateRange) + { + query.where("t.date >= ?").bind(params.dateRange->begin); + query.where("t.date <= ?").bind(params.dateRange->end); + } + + for (std::string_view keyword : params.keywords) query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%"); - if (!clusterIds.empty()) + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_release s_r ON s_r.release_id = r.id") + .where("s_r.user_id = ?").bind(params.starringUser) + .where("s_r.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "r.id IN (SELECT DISTINCT r.id FROM release r" @@ -58,18 +75,40 @@ createQuery(Session& session, " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case ReleaseSortMethod::None: + break; + case ReleaseSortMethod::Name: + query.orderBy("r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ReleaseSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case ReleaseSortMethod::Date: + query.orderBy("t.date, r.name COLLATE NOCASE"); + break; + case ReleaseSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_r.date_time DESC"); + break; + } + return query; } @@ -80,7 +119,7 @@ _MBID {MBID ? MBID->getAsString() : ""} } std::vector -Release::getByName(Session& session, const std::string& name) +Release::find(Session& session, const std::string& name) { session.checkUniqueLocked(); @@ -93,7 +132,7 @@ Release::getByName(Session& session, const std::string& name) } Release::pointer -Release::getByMBID(Session& session, const UUID& mbid) +Release::find(Session& session, const UUID& mbid) { session.checkSharedLocked(); @@ -104,7 +143,7 @@ Release::getByMBID(Session& session, const UUID& mbid) } Release::pointer -Release::getById(Session& session, ReleaseId id) +Release::find(Session& session, ReleaseId id) { session.checkSharedLocked(); @@ -137,230 +176,44 @@ Release::getCount(Session& session) { session.checkSharedLocked(); - return session.getDboSession().find().resultList().size(); + return session.getDboSession().query("SELECT COUNT(*) FROM release"); } -std::vector -Release::getAll(Session& session, std::optional range) +RangeResults +Release::findOrderedByArtist(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find() - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) : -1) - .orderBy("name COLLATE NOCASE") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM release"); - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getAllOrderedByArtist(Session& session, std::optional offset, std::optional size) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>( - "SELECT DISTINCT r FROM release r" + // TODO merge with execQuery + auto query {session.getDboSession().query( + "SELECT DISTINCT r.id FROM release r" " INNER JOIN track t ON r.id = t.release_id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN artist a ON t_a_l.artist_id = a.id") - .offset(offset ? static_cast(*offset) : -1) - .limit(size ? static_cast(*size) : -1) - .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") - .resultList()}; + .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } -std::vector -Release::getAllRandom(Session& session, const std::vector& clusterIds, std::optional size) +RangeResults +Release::findOrphans(Session& session, Range range) { session.checkSharedLocked(); - auto query {createQuery>(session, "SELECT DISTINCT r from release r", clusterIds, {})}; - auto res {query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")}; + return execQuery(query, range); } -std::vector -Release::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional size) +RangeResults +Release::find(Session& session, const FindParameters& params) { session.checkSharedLocked(); - auto query {createQuery(session, "SELECT DISTINCT r.id from release r", clusterIds, {})}; + auto query {createQuery(session, params)}; - Wt::Dbo::collection res = query - .orderBy("RANDOM()") - .limit(size ? static_cast(*size) : -1); - - return std::vector(res.begin(), res.end()); + return execQuery(query, params.range); } - -std::vector -Release::getAllOrphans(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL").resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getLastWritten(Session& session, - std::optional after, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT r from release r", clusterIds, {})}; - if (after) - query.where("t.file_last_write > ?").bind(after); - - auto collection {query - .orderBy("t.file_last_write DESC") - .groupBy("r.id") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional range) -{ - auto res {session.getDboSession().query> - ("SELECT DISTINCT r from release r INNER JOIN track t ON r.id = t.release_id") - .where("t.date >= ?").bind(Wt::WDate {yearFrom, 1, 1}) - .where("t.date <= ?").bind(Wt::WDate {yearTo, 12, 31}) - .orderBy("t.date, r.name COLLATE NOCASE") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) : -1) - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -Release::getStarred(Session& session, - User::pointer user, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT r from release r", clusterIds, {})}; - { - std::ostringstream oss; - oss << "r.id IN (SELECT DISTINCT r.id FROM release r" - " INNER JOIN user_release_starred urs ON urs.release_id = r.id" - " INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)"; - - query.bind(user->getId()); - query.where(oss.str()); - } - - auto collection {query - .groupBy("r.id") - .orderBy("r.name COLLATE NOCASE") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; - - -} - -std::vector -Release::getByClusters(Session& session, const std::vector& clusters) -{ - assert(!clusters.empty()); - - session.checkSharedLocked(); - - bool moreResults; - return getByFilter(session, clusters, {}, std::nullopt, moreResults); -} - -std::vector -Release::getByFilter(Session& session, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional range, - bool& moreResults) -{ - session.checkSharedLocked(); - - auto collection {createQuery>(session, "SELECT r from release r", clusterIds, keywords) - .groupBy("r.id") - .orderBy("r.name COLLATE NOCASE") - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; -} - -std::vector -Release::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT r.id FROM release r" - " INNER JOIN track t ON t.release_id = r.id" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - - std::optional Release::getTotalTrack(void) const { diff --git a/src/libs/services/database/impl/ScanSettings.cpp b/src/libs/services/database/impl/ScanSettings.cpp index 0b509f32..70f08b72 100644 --- a/src/libs/services/database/impl/ScanSettings.cpp +++ b/src/libs/services/database/impl/ScanSettings.cpp @@ -111,7 +111,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set& clu // Create any missing cluster type for (const std::string& clusterTypeName : clusterTypeNames) { - ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)}; + ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)}; if (!clusterType) { LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName; diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp index 7b219a08..76e0396c 100644 --- a/src/libs/services/database/impl/Session.cpp +++ b/src/libs/services/database/impl/Session.cpp @@ -28,10 +28,15 @@ #include "utils/Logger.hpp" #include "services/database/Artist.hpp" +#include "services/database/AuthToken.hpp" #include "services/database/Cluster.hpp" #include "services/database/Db.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/ScanSettings.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" #include "services/database/Track.hpp" #include "services/database/TrackBookmark.hpp" #include "services/database/TrackArtistLink.hpp" @@ -43,7 +48,7 @@ namespace Database { using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION {31}; + static constexpr Version LMS_DATABASE_VERSION {32}; class VersionInfo { @@ -360,6 +365,61 @@ CREATE TABLE "track_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 31) + { + // new star system, using dedicated ObjectSets per scrobbler + _session.execute("DROP TABLE user_artist_starred"); + _session.execute("DROP TABLE user_release_starred"); + _session.execute("DROP TABLE user_track_starred"); + + _session.execute(R"( +CREATE TABLE IF NOT EXISTS "starred_artist" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "artist_id" bigint, + "user_id" bigint, + constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred, + constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + _session.execute(R"( +CREATE TABLE IF NOT EXISTS "starred_release" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "release_id" bigint, + "user_id" bigint, + constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred, + constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + _session.execute(R"( +CREATE TABLE IF NOT EXISTS "starred_track" ( + "id" integer primary key autoincrement, + "version" integer not null, + "scrobbler" integer not null, + "date_time" text, + "track_id" bigint, + "user_id" bigint, + constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + + _session.execute(R"( +CREATE TABLE IF NOT EXISTS "listen" ( + "id" integer primary key autoincrement, + "version" integer not null, + "date_time" text, + "scrobbler" integer not null, + "track_id" bigint, + "user_id" bigint, + constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, + constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred +))"); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; @@ -380,8 +440,12 @@ Session::Session(Db& db) _session.mapClass("auth_token"); _session.mapClass("cluster"); _session.mapClass("cluster_type"); + _session.mapClass("listen"); _session.mapClass("release"); _session.mapClass("scan_settings"); + _session.mapClass("starred_artist"); + _session.mapClass("starred_release"); + _session.mapClass("starred_track"); _session.mapClass("track"); _session.mapClass("track_bookmark"); _session.mapClass("track_artist_link"); @@ -484,6 +548,11 @@ Session::prepareTables() _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)"); } // Initial settings tables diff --git a/src/libs/services/database/impl/StarredArtist.cpp b/src/libs/services/database/impl/StarredArtist.cpp new file mode 100644 index 00000000..ed617665 --- /dev/null +++ b/src/libs/services/database/impl/StarredArtist.cpp @@ -0,0 +1,79 @@ +/* + * 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 "services/database/StarredArtist.hpp" + +#include + +#include "services/database/Artist.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredArtist::StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _artist {getDboPtr(artist)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredArtist::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_artist"); + } + + StarredArtist::pointer + StarredArtist::find(Session& session, StarredArtistId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredArtist::pointer + StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("artist_id = ?").bind(artistId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredArtist::pointer + StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredArtist::pointer res {session.getDboSession().add(std::make_unique(artist, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredArtist::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/services/database/impl/StarredRelease.cpp b/src/libs/services/database/impl/StarredRelease.cpp new file mode 100644 index 00000000..c72331d5 --- /dev/null +++ b/src/libs/services/database/impl/StarredRelease.cpp @@ -0,0 +1,79 @@ +/* + * 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 "services/database/StarredRelease.hpp" + +#include + +#include "services/database/Release.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredRelease::StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _release {getDboPtr(release)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredRelease::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_release"); + } + + StarredRelease::pointer + StarredRelease::find(Session& session, StarredReleaseId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredRelease::pointer + StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("release_id = ?").bind(releaseId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredRelease::pointer + StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredRelease::pointer res {session.getDboSession().add(std::make_unique(release, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredRelease::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp new file mode 100644 index 00000000..6543654f --- /dev/null +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -0,0 +1,79 @@ +/* + * 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 "services/database/StarredTrack.hpp" + +#include + +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" + +namespace Database +{ + StarredTrack::StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) + : _scrobbler {scrobbler} + , _track {getDboPtr(track)} + , _user {getDboPtr(user)} + { + } + + std::size_t + StarredTrack::getCount(Session& session) + { + session.checkSharedLocked(); + return session.getDboSession().query("SELECT COUNT(*) FROM starred_track"); + } + + StarredTrack::pointer + StarredTrack::find(Session& session, StarredTrackId id) + { + session.checkSharedLocked(); + return session.getDboSession().find().where("id = ?").bind(id).resultValue(); + } + + StarredTrack::pointer + StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .where("user_id = ?").bind(userId) + .where("scrobbler = ?").bind(scrobbler) + .resultValue(); + } + + StarredTrack::pointer + StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) + { + session.checkUniqueLocked(); + + StarredTrack::pointer res {session.getDboSession().add(std::make_unique(track, user, scrobbler))}; + session.getDboSession().flush(); + + return res; + } + + void + StarredTrack::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = normalizeDateTime(dateTime); + } +} diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index d1968146..a93f2339 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -30,29 +30,36 @@ #include "services/database/User.hpp" #include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" -#include "Traits.hpp" #include "Utils.hpp" namespace Database { -template static -Wt::Dbo::Query -createQuery(Session& session, - const std::string& queryStr, - const std::vector& clusterIds, - const std::vector& keywords) +Wt::Dbo::Query +createQuery(Session& session, const Track::FindParameters& params) { session.checkSharedLocked(); - auto query {session.getDboSession().query(queryStr)}; + auto query {session.getDboSession().query("SELECT t.id from track t")}; - for (std::string_view keyword : keywords) + for (std::string_view keyword : params.keywords) query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%"); - if (!clusterIds.empty()) + if (params.writtenAfter.isValid()) + query.where("t.file_last_write > ?").bind(params.writtenAfter); + + if (params.starringUser.isValid()) + { + assert(params.scrobbler); + query.join("starred_track s_t ON s_t.track_id = t.id") + .where("s_t.user_id = ?").bind(params.starringUser) + .where("s_t.scrobbler = ?").bind(*params.scrobbler); + } + + if (!params.clusters.empty()) { std::ostringstream oss; oss << "t.id IN (SELECT DISTINCT t.id FROM track t" @@ -60,18 +67,34 @@ createQuery(Session& session, " INNER JOIN cluster c ON c.id = t_c.cluster_id"; WhereClause clusterClause; - for (const ClusterId clusterId : clusterIds) + for (const ClusterId clusterId : params.clusters) { clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId); } oss << " " << clusterClause.get(); - oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; + oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; query.where(oss.str()); } + switch (params.sortMethod) + { + case TrackSortMethod::None: + break; + case TrackSortMethod::LastWritten: + query.orderBy("t.file_last_write DESC"); + break; + case TrackSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case TrackSortMethod::StarredDateDesc: + assert(params.starringUser.isValid()); + query.orderBy("s_t.date_time DESC"); + break; + } + return query; } @@ -80,6 +103,17 @@ Track::Track(const std::filesystem::path& p) { } +Track::pointer +Track::create(Session& session, const std::filesystem::path& p) +{ + session.checkUniqueLocked(); + + Track::pointer res {session.getDboSession().add(std::make_unique(p))}; + session.getDboSession().flush(); + + return res; +} + std::size_t Track::getCount(Session& session) { @@ -88,58 +122,8 @@ Track::getCount(Session& session) return session.getDboSession().query("SELECT COUNT(*) FROM track"); } -std::vector -Track::getAll(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .limit(limit ? static_cast(*limit) : -1) - .resultList()}; - - return std::vector(std::cbegin(res), std::cend(res)); -} - -std::vector -Track::getAllRandom(Session& session, const std::vector& clusterIds, std::optional limit) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - auto collection {query - .orderBy("RANDOM()") - .limit(limit ? static_cast(*limit) + 1: -1) - .resultList()}; - - return std::vector(collection.begin(), collection.end()); -} - -std::vector -Track::getAllIdsRandom(Session& session, const std::vector& clusterIds, std::optional limit) -{ - session.checkSharedLocked(); - - auto query {createQuery(session, "SELECT t.id from track t", clusterIds, {})}; - - Wt::Dbo::collection collection = query - .orderBy("RANDOM()") - .limit(limit ? static_cast(*limit) + 1: -1); - - return std::vector(collection.begin(), collection.end()); -} - - -std::vector -Track::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query("SELECT id FROM track"); - return std::vector(res.begin(), res.end()); -} - Track::pointer -Track::getByPath(Session& session, const std::filesystem::path& p) +Track::findByPath(Session& session, const std::filesystem::path& p) { session.checkSharedLocked(); @@ -147,7 +131,7 @@ Track::getByPath(Session& session, const std::filesystem::path& p) } Track::pointer -Track::getById(Session& session, TrackId id) +Track::find(Session& session, TrackId id) { session.checkSharedLocked(); @@ -165,7 +149,7 @@ Track::exists(Session& session, TrackId id) } std::vector -Track::getByRecordingMBID(Session& session, const UUID& mbid) +Track::findByRecordingMBID(Session& session, const UUID& mbid) { session.checkSharedLocked(); @@ -176,151 +160,52 @@ Track::getByRecordingMBID(Session& session, const UUID& mbid) return std::vector(res.begin(), res.end()); } -Track::pointer -Track::create(Session& session, const std::filesystem::path& p) -{ - session.checkUniqueLocked(); - - Track::pointer res {session.getDboSession().add(std::make_unique(p))}; - session.getDboSession().flush(); - - return res; -} - -std::vector> -Track::getAllPaths(Session& session, std::optional offset, std::optional size) +RangeResults +Track::findPaths(Session& session, Range range) { using QueryResultType = std::tuple; session.checkSharedLocked(); - Wt::Dbo::collection queryRes = session.getDboSession().query("SELECT id,file_path FROM track") - .limit(size ? static_cast(*size) + 1 : -1) - .offset(offset ? static_cast(*offset) : -1); + // TODO Dbo traits on filesystem + auto query {session.getDboSession().query("SELECT id, file_path FROM track")}; - std::vector> result; - result.reserve(queryRes.size()); + RangeResults queryResults {execQuery(query, range)}; - std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result), + RangeResults res; + res.range = queryResults.range; + res.moreResults = queryResults.moreResults; + res.results.reserve(queryResults.results.size()); + + std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), [](const QueryResultType& queryResult) { - return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult)); + return PathResult {std::get<0>(queryResult), std::get<1>(queryResult)}; }); - return result; -} - -std::vector -Track::getMBIDDuplicates(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query>( "SELECT track FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") - .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") - .resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getLastWritten(Session& session, std::optional after, const std::vector& clusterIds, std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - if (after) - query.where("t.file_last_write > ?").bind(after); - - auto collection {query - .orderBy("t.file_last_write DESC") - .groupBy("t.id") - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - return res; } -std::vector -Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session) +RangeResults +Track::findMBIDDuplicates(Session& session, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().query> - ("SELECT t FROM track t") + auto query {session.getDboSession().query( "SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") + .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid")}; + + return execQuery(query, range); +} + +RangeResults +Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT t.id FROM track t") .where("LENGTH(t.recording_mbid) > 0") - .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") - .resultList()}; - return std::vector(res.begin(), res.end()); -} + .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")}; -std::vector -Track::getAllIdsWithFeatures(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT t.id FROM track t") - .where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getAllIdsWithClusters(Session& session, std::optional limit) -{ - session.checkSharedLocked(); - - Wt::Dbo::collection res = session.getDboSession().query - ("SELECT DISTINCT t.id FROM track t" - " INNER JOIN track_cluster t_c ON t_c.track_id = t.id") - .limit(limit ? static_cast(*limit) : -1); - - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getStarred(Session& session, - ObjectPtr user, - const std::vector& clusterIds, - std::optional range, bool& moreResults) -{ - session.checkSharedLocked(); - - auto query {createQuery>(session, "SELECT t from track t", clusterIds, {})}; - { - std::ostringstream oss; - oss << "t.id IN (SELECT DISTINCT t.id FROM track t" - " INNER JOIN user_track_starred uts ON uts.track_id = t.id" - " INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)"; - - query.bind(user->getId().toString()); - query.where(oss.str()); - } - - auto collection {query - .offset(range ? static_cast(range->offset) : -1) - .limit(range ? static_cast(range->limit) + 1: -1) - .resultList()}; - - std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, range); } std::vector @@ -342,56 +227,31 @@ Track::getClusterIds() const return std::vector(res.begin(), res.end()); } -bool -Track::hasTrackFeatures() const -{ - return (_trackFeatures.lock() != Wt::Dbo::ptr {}); -} - -std::vector -Track::getByFilter(Session& session, - const std::vector& clusterIds, - const std::vector& keywords, - std::optional range, - bool& moreResults) +RangeResults +Track::find(Session& session, const FindParameters& parameters) { session.checkSharedLocked(); - auto collection {createQuery>(session, "SELECT t from track t", clusterIds, keywords) - .limit(range ? static_cast(range->limit) + 1 : -1) - .offset(range ? static_cast(range->offset) : -1) - .resultList()}; + auto query {createQuery(session, parameters)}; - std::vector res(collection.begin(), collection.end()); - if (range && (res.size() == static_cast(range->limit) + 1)) - { - moreResults = true; - res.pop_back(); - } - else - moreResults = false; - - return res; + return execQuery(query, parameters.range); } -std::vector -Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName) +RangeResults +Track::findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName) { session.checkSharedLocked(); - auto res {session.getDboSession().query>("SELECT t from track t") + auto query {session.getDboSession().query("SELECT t.id from track t") .join("release r ON t.release_id = r.id") .where("t.name = ?").bind(trackName) - .where("r.name = ?").bind(releaseName) - .resultList()}; - return std::vector(res.begin(), res.end()); + .where("r.name = ?").bind(releaseName)}; + + return execQuery(query, Range {}); } -std::vector -Track::getSimilarTracks(Session& session, - const std::vector& tracks, - std::optional offset, - std::optional size) +RangeResults +Track::findSimilarTracks(Session& session, const std::vector& tracks, Range range) { assert(!tracks.empty()); session.checkSharedLocked(); @@ -404,15 +264,13 @@ Track::getSimilarTracks(Session& session, oss << "?"; } - auto query {session.getDboSession().query>( - "SELECT t FROM track t" + auto query {session.getDboSession().query( + "SELECT t.id FROM track t" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))" " AND t.id NOT IN (" + oss.str() + ")") .groupBy("t.id") - .orderBy("COUNT(*) DESC, RANDOM()") - .limit(size ? static_cast(*size) : -1) - .offset(offset ? static_cast(*offset) : -1)}; + .orderBy("COUNT(*) DESC, RANDOM()")}; for (TrackId trackId : tracks) query.bind(trackId); @@ -420,22 +278,7 @@ Track::getSimilarTracks(Session& session, for (TrackId trackId : tracks) query.bind(trackId); - auto res {query.resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -Track::getByClusters(Session& session, const std::vector& clusters) -{ - assert(!clusters.empty()); - session.checkSharedLocked(); - - bool moreResults; - return getByFilter(session, - clusters, - {}, // keywords - std::nullopt, // range - moreResults); + return execQuery(query, range); } void @@ -458,12 +301,6 @@ Track::setClusters(const std::vector>& clusters) _clusters.insert(getDboPtr(cluster)); } -void -Track::setFeatures(const ObjectPtr& features) -{ - _trackFeatures = getDboPtr(features); -} - std::optional Track::getTrackNumber() const { @@ -593,12 +430,6 @@ Track::getArtistLinks() const return std::vector(_trackArtistLinks.begin(), _trackArtistLinks.end()); } -ObjectPtr -Track::getTrackFeatures() const -{ - return _trackFeatures.lock(); -} - std::vector> Track::getClusterGroups(const std::vector& clusterTypes, std::size_t size) const { diff --git a/src/libs/services/database/impl/TrackArtistLink.cpp b/src/libs/services/database/impl/TrackArtistLink.cpp index 03bbbbee..d54339e8 100644 --- a/src/libs/services/database/impl/TrackArtistLink.cpp +++ b/src/libs/services/database/impl/TrackArtistLink.cpp @@ -23,7 +23,7 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" namespace Database { @@ -46,7 +46,7 @@ TrackArtistLink::create(Session& session, ObjectPtr track, ObjectPtr -TrackArtistLink::getUsedTypes(Session& session) +TrackArtistLink::findUsedTypes(Session& session) { session.checkSharedLocked(); diff --git a/src/libs/services/database/impl/TrackBookmark.cpp b/src/libs/services/database/impl/TrackBookmark.cpp index 5a963c3c..28e169ca 100644 --- a/src/libs/services/database/impl/TrackBookmark.cpp +++ b/src/libs/services/database/impl/TrackBookmark.cpp @@ -22,7 +22,8 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/User.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -32,6 +33,15 @@ _track {getDboPtr(track)} { } +std::size_t +TrackBookmark::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track_bookmark"); +} + + TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr track) { @@ -43,40 +53,30 @@ TrackBookmark::create(Session& session, ObjectPtr user, ObjectPtr t return res; } -std::vector -TrackBookmark::getAll(Session& session) +RangeResults +TrackBookmark::find(Session& session, UserId userId, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find().resultList()}; - return std::vector(std::cbegin(res), std::cend(res)); -} + auto query {session.getDboSession().query("SELECT id from track_bookmark") + .where("user_id = ?").bind(userId)}; -std::vector -TrackBookmark::getByUser(Session& session, User::pointer user) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .resultList()}; - - return std::vector(std::cbegin(res), std::cend(res)); + return execQuery(query, range); } TrackBookmark::pointer -TrackBookmark::getByUser(Session& session, ObjectPtr user, ObjectPtr track) +TrackBookmark::find(Session& session, UserId userId, TrackId trackId) { session.checkSharedLocked(); return session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .where("track_id = ?").bind(track->getId()) + .where("user_id = ?").bind(userId) + .where("track_id = ?").bind(trackId) .resultValue(); } TrackBookmark::pointer -TrackBookmark::getById(Session& session, TrackBookmarkId id) +TrackBookmark::find(Session& session, TrackBookmarkId id) { session.checkSharedLocked(); @@ -85,6 +85,5 @@ TrackBookmark::getById(Session& session, TrackBookmarkId id) .resultValue(); } - } // namespace Database diff --git a/src/libs/services/database/impl/TrackFeatures.cpp b/src/libs/services/database/impl/TrackFeatures.cpp index 65cc9b7a..628c10c7 100644 --- a/src/libs/services/database/impl/TrackFeatures.cpp +++ b/src/libs/services/database/impl/TrackFeatures.cpp @@ -25,6 +25,8 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -34,11 +36,53 @@ _track {getDboPtr(track)} { } +std::size_t +TrackFeatures::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM track_features"); +} + +TrackFeatures::pointer +TrackFeatures::find(Session& session, TrackFeaturesId id) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("id = ?").bind(id) + .resultValue(); +} + +TrackFeatures::pointer +TrackFeatures::find(Session& session, TrackId trackId) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .resultValue(); +} + +RangeResults +TrackFeatures::find(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT id from track_features")}; + + return execQuery(query, range); +} + TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures) { session.checkUniqueLocked(); - return session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures)); + + TrackFeatures::pointer res {session.getDboSession().add(std::make_unique(track, jsonEncodedFeatures))}; + session.getDboSession().flush(); + + return res; } FeatureValues @@ -51,6 +95,8 @@ TrackFeatures::getFeatureValues(const FeatureName& featureNode) const FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set& featureNames) const { + FeatureValuesMap res; + try { std::istringstream iss {_data}; @@ -58,7 +104,6 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set& featur boost::property_tree::read_json(iss, root); - FeatureValuesMap res; for (const FeatureName& featureName : featureNames) { FeatureValues& featureValues {res[featureName]}; @@ -75,14 +120,14 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set& featur if (!hasChildren) featureValues.push_back(node.get_value()); } - - return res; } catch (boost::property_tree::ptree_error& error) { LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); - return {}; + res.clear(); } + + return res; } } // namespace Database diff --git a/src/libs/services/database/impl/TrackList.cpp b/src/libs/services/database/impl/TrackList.cpp index 8e69434b..c91826fd 100644 --- a/src/libs/services/database/impl/TrackList.cpp +++ b/src/libs/services/database/impl/TrackList.cpp @@ -30,7 +30,8 @@ #include "services/database/Track.hpp" #include "SqlQuery.hpp" #include "StringViewTraits.hpp" -#include "Traits.hpp" +#include "IdTypeTraits.hpp" +#include "Utils.hpp" namespace Database { @@ -55,56 +56,54 @@ TrackList::create(Session& session, std::string_view name, Type type, bool isPub return res; } -TrackList::pointer -TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr user) +std::size_t +TrackList::getCount(Session& session) { session.checkSharedLocked(); - assert(user); + + return session.getDboSession().query("SELECT COUNT(*) FROM tracklist"); +} + + +TrackList::pointer +TrackList::find(Session& session, std::string_view name, Type type, UserId userId) +{ + session.checkSharedLocked(); + assert(userId.isValid()); return session.getDboSession().find() .where("name = ?").bind(name) .where("type = ?").bind(type) - .where("user_id = ?").bind(user->getId()).resultValue(); + .where("user_id = ?").bind(userId).resultValue(); } -std::vector -TrackList::getAll(Session& session) +RangeResults +TrackList::find(Session& session, UserId userId, Range range) { session.checkSharedLocked(); - auto res = session.getDboSession().find().resultList(); - return std::vector(res.begin(), res.end()); + auto query {session.getDboSession().query("SELECT id FROM tracklist") + .where("user_id = ?").bind(userId) + .orderBy("name COLLATE NOCASE")}; + + return execQuery(query, range); } -std::vector -TrackList::getAll(Session& session, ObjectPtr user) +RangeResults +TrackList::find(Session& session, UserId userId, Type type, Range range) { session.checkSharedLocked(); - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) - .orderBy("name COLLATE NOCASE") - .resultList()}; - - return std::vector(res.begin(), res.end()); -} - -std::vector -TrackList::getAll(Session& session, ObjectPtr user, Type type) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find() - .where("user_id = ?").bind(user->getId()) + auto query {session.getDboSession().query("SELECT id FROM tracklist") + .where("user_id = ?").bind(userId) .where("type = ?").bind(type) - .orderBy("name COLLATE NOCASE") - .resultList()}; + .orderBy("name COLLATE NOCASE")}; - return std::vector(res.begin(), res.end()); + return execQuery(query, range); } TrackList::pointer -TrackList::getById(Session& session, TrackListId id) +TrackList::find(Session& session, TrackListId id) { session.checkSharedLocked(); @@ -159,7 +158,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTim return session()->find() .where("tracklist_id = ?").bind(getId()) .where("track_id = ?").bind(track->getId()) - .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) + .where("date_time = ?").bind(normalizeDateTime(dateTime)) .resultValue(); } @@ -270,19 +269,109 @@ createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std: } std::vector -TrackList::getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const +TrackList::getArtists(const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) + .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1)}; + + switch (sortMethod) + { + case ArtistSortMethod::None: + break; + case ArtistSortMethod::ByName: + query.orderBy("a.name COLLATE NOCASE"); + break; + case ArtistSortMethod::BySortName: + query.orderBy("a.sort_name COLLATE NOCASE"); + break; + case ArtistSortMethod::Random: + query.orderBy("RANDOM()"); + break; + case ArtistSortMethod::LastWritten: + case ArtistSortMethod::StarredDateDesc: + assert(false); // Not implemented! + break; + } + + Wt::Dbo::collection> collection {query.resultList()}; + + auto res {std::vector(collection.begin(), collection.end())}; + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + + +std::vector> +TrackList::getReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) + .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + + std::vector res(collection.begin(), collection.end()); + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + +std::vector> +TrackList::getTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const +{ + assert(session()); + + auto collection {createTracksQuery(*session(), getId(), clusterIds) + .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") + .limit(range ? static_cast(range->size) + 1 : -1) + .offset(range ? static_cast(range->offset) : -1) + .resultList()}; + + std::vector res(collection.begin(), collection.end()); + if (range && res.size() == static_cast(range->size) + 1) + { + moreResults = true; + res.pop_back(); + } + else + moreResults = false; + + return res; +} + +std::vector +TrackList::getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const { assert(session()); auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) .groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; auto res {std::vector(collection.begin(), collection.end())}; - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -294,19 +383,19 @@ TrackList::getArtistsReverse(const std::vector& clusterIds, std::opti } std::vector -TrackList::getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const +TrackList::getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds) .groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -318,19 +407,19 @@ TrackList::getReleasesReverse(const std::vector& clusterIds, std::opt } std::vector -TrackList::getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const +TrackList::getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const { assert(session()); auto collection {createTracksQuery(*session(), getId(), clusterIds) .groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)") - .orderBy("p_e.date_time DESC") - .limit(range ? static_cast(range->limit) + 1 : -1) + .orderBy("p_e.date_time DESC, p_e.id DESC") + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -424,13 +513,13 @@ TrackList::getTopArtists(const std::vector& clusterIds, std::optional auto collection {query .orderBy("COUNT(a.id) DESC") .groupBy("a.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -450,12 +539,12 @@ TrackList::getTopReleases(const std::vector& clusterIds, std::optiona auto collection {query .orderBy("COUNT(r.id) DESC") .groupBy("r.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -475,12 +564,12 @@ TrackList::getTopTracks(const std::vector& clusterIds, std::optional< auto collection {query .orderBy("COUNT(t.id) DESC") .groupBy("t.id") - .limit(range ? static_cast(range->limit) + 1 : -1) + .limit(range ? static_cast(range->size) + 1 : -1) .offset(range ? static_cast(range->offset) : -1) .resultList()}; std::vector res(collection.begin(), collection.end()); - if (range && res.size() == static_cast(range->limit) + 1) + if (range && res.size() == static_cast(range->size) + 1) { moreResults = true; res.pop_back(); @@ -492,11 +581,10 @@ TrackList::getTopTracks(const std::vector& clusterIds, std::optional< } TrackListEntry::TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime) -: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution +: _dateTime {normalizeDateTime(dateTime)} , _track {getDboPtr(track)} , _tracklist {getDboPtr(tracklist)} { - assert(_dateTime.isValid()); } TrackListEntry::pointer diff --git a/src/libs/services/database/impl/Types.cpp b/src/libs/services/database/impl/Types.cpp new file mode 100644 index 00000000..ef02556d --- /dev/null +++ b/src/libs/services/database/impl/Types.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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 "services/database/Types.hpp" + +#include + +namespace Database +{ + static const std::set allowedAudioBitrates + { + 64000, + 96000, + 128000, + 192000, + 320000, + }; + + void visitAllowedAudioBitrates(std::function func) + { + for (Bitrate bitrate : allowedAudioBitrates) + func(bitrate); + } + + bool isAudioBitrateAllowed(Bitrate bitrate) + { + return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates); + } + + DateRange + DateRange::fromYearRange(int from, int to) + { + return DateRange {{from, 1, 1}, {to, 12, 31}}; + } +} + diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index 75c69adf..a8abea66 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -23,85 +23,18 @@ #include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" -#include "services/database/TrackList.hpp" #include "utils/Logger.hpp" +#include "IdTypeTraits.hpp" #include "StringViewTraits.hpp" -#include "Traits.hpp" +#include "Utils.hpp" namespace Database { - -AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user) -: _value {value} -, _expiry {expiry} -, _user {getDboPtr(user)} -{ - -} - -AuthToken::pointer -AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user) -{ - session.checkUniqueLocked(); - - AuthToken::pointer res {session.getDboSession().add(std::make_unique(value, expiry, user))}; - session.getDboSession().flush(); - - return res; -} - -void -AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) -{ - session.checkUniqueLocked(); - - session.getDboSession().execute - ("DELETE FROM auth_token WHERE expiry < ?").bind(now); -} - -AuthToken::pointer -AuthToken::getByValue(Session& session, const std::string& value) -{ - session.checkSharedLocked(); - - return session.getDboSession().find() - .where("value = ?").bind(value) - .resultValue(); -} - -static const std::string queuedListName {"__queued_tracks__"}; - User::User(std::string_view loginName) : _loginName {loginName} { } -std::vector -User::getAll(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().find().resultList()}; - return std::vector(res.begin(), res.end()); -} - -std::vector -User::getAllIds(Session& session) -{ - session.checkSharedLocked(); - - auto res {session.getDboSession().query("SELECT id FROM user").resultList()}; - return std::vector(res.begin(), res.end()); -} - -User::pointer -User::getDemo(Session& session) -{ - session.checkSharedLocked(); - - return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); -} - std::size_t User::getCount(Session& session) { @@ -110,28 +43,43 @@ User::getCount(Session& session) return session.getDboSession().query("SELECT COUNT(*) FROM user"); } +RangeResults +User::find(Session& session, Range range) +{ + session.checkSharedLocked(); + + auto query {session.getDboSession().query("SELECT id FROM user")}; + + return execQuery(query, range); +} + +User::pointer +User::findDemoUser(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().find().where("type = ?").bind(UserType::DEMO).resultValue(); +} + User::pointer User::create(Session& session, std::string_view loginName) { session.checkUniqueLocked(); User::pointer user {session.getDboSession().add(std::make_unique(loginName))}; - - TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user); - session.getDboSession().flush(); return user; } User::pointer -User::getById(Session& session, UserId id) +User::find(Session& session, UserId id) { return session.getDboSession().find().where("id = ?").bind(id).resultValue(); } User::pointer -User::getByLoginName(Session& session, std::string_view name) +User::find(Session& session, std::string_view name) { return session.getDboSession().find() .where("login_name = ?").bind(name) @@ -141,7 +89,7 @@ User::getByLoginName(Session& session, std::string_view name) void User::setSubsonicTranscodeBitrate(Bitrate bitrate) { - assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend()); + assert(isAudioBitrateAllowed(bitrate)); _subsonicTranscodeBitrate = bitrate; } @@ -151,75 +99,6 @@ User::clearAuthTokens() _authTokens.clear(); } -TrackList::pointer -User::getQueuedTrackList(Session& session) const -{ - assert(self()); - session.checkSharedLocked(); - - return TrackList::get(session, queuedListName, TrackList::Type::Internal, self()); -} - -void -User::star(ObjectPtr artist) -{ - if (_starredArtists.count(getDboPtr(artist)) == 0) - _starredArtists.insert(getDboPtr(artist)); -} - -void -User::unstar(ObjectPtr artist) -{ - if (_starredArtists.count(getDboPtr(artist)) != 0) - _starredArtists.erase(getDboPtr(artist)); -} - -bool -User::isStarred(ObjectPtr artist) const -{ - return _starredArtists.count(getDboPtr(artist)) != 0; -} - -void -User::star(ObjectPtr release) -{ - if (_starredReleases.count(getDboPtr(release)) == 0) - _starredReleases.insert(getDboPtr(release)); -} - -void -User::unstar(ObjectPtr release) -{ - if (_starredReleases.count(getDboPtr(release)) != 0) - _starredReleases.erase(getDboPtr(release)); -} - -bool -User::isStarred(ObjectPtr release) const -{ - return _starredReleases.count(getDboPtr(release)) != 0; -} - -void -User::star(ObjectPtr track) -{ - if (_starredTracks.count(getDboPtr(track)) == 0) - _starredTracks.insert(getDboPtr(track)); -} - -void -User::unstar(ObjectPtr track) -{ - if (_starredTracks.count(getDboPtr(track)) != 0) - _starredTracks.erase(getDboPtr(track)); -} - -bool -User::isStarred(ObjectPtr track) const -{ - return _starredTracks.count(getDboPtr(track)) != 0; -} - } // namespace Database diff --git a/src/libs/services/database/impl/Utils.cpp b/src/libs/services/database/impl/Utils.cpp index 39849db1..e0994090 100644 --- a/src/libs/services/database/impl/Utils.cpp +++ b/src/libs/services/database/impl/Utils.cpp @@ -29,5 +29,12 @@ namespace Database return StringUtils::escapeString(keyword, "%_", escapeChar); } + Wt::WDateTime + normalizeDateTime(const Wt::WDateTime& dateTime) + { + // force second resolution + return Wt::WDateTime::fromTime_t(dateTime.toTime_t()); + } + } // namespace Database diff --git a/src/libs/services/database/impl/Utils.hpp b/src/libs/services/database/impl/Utils.hpp index 547c7995..b92b12c3 100644 --- a/src/libs/services/database/impl/Utils.hpp +++ b/src/libs/services/database/impl/Utils.hpp @@ -21,13 +21,66 @@ #include #include -#include + +#include +#include + +#include "services/database/Types.hpp" namespace Database { #define ESCAPE_CHAR_STR "\\" - static constexpr char escapeChar {'\\'}; + static inline constexpr char escapeChar {'\\'}; std::string escapeLikeKeyword(std::string_view keywords); + template + RangeResults + execQuery(Wt::Dbo::Query& query, Range range) + { + RangeResults res; + + auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) + .offset(range.offset ? static_cast(range.offset) : -1) + .resultList()}; + + res.results.assign(collection.begin(), collection.end()); + if (range.size && res.results.size() == static_cast(range.size) + 1) + { + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; + + res.range.offset = range.offset; + res.range.size = res.results.size(); + return res; + } + + template + RangeResults + execQuery(Wt::Dbo::Query>& query, Range range) + { + RangeResults res; + + auto collection {query.limit(range.size ? static_cast(range.size) + 1 : -1) + .offset(range.offset ? static_cast(range.offset) : -1) + .resultList()}; + + res.results.assign(collection.begin(), collection.end()); + if (range.size && res.results.size() == static_cast(range.size) + 1) + { + res.moreResults = true; + res.results.pop_back(); + } + else + res.moreResults = false; + + res.range.offset = range.offset; + res.range.size = res.results.size(); + return res; + } + + Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); } // namespace Database diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index 0f5785bc..7f28e2fa 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -27,7 +27,11 @@ #include #include +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "utils/EnumSet.hpp" #include "utils/UUID.hpp" @@ -38,6 +42,7 @@ class Cluster; class ClusterType; class Release; class Session; +class StarredArtist; class Track; class TrackArtistLink; class User; @@ -45,67 +50,53 @@ class User; class Artist : public Object { public: - enum class SortMethod + struct FindParameters { - None, - ByName, - BySortName, + std::vector clusters; // if non empty, at least one artist that belongs to these clusters + 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 + ArtistSortMethod sortMethod {ArtistSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only artists starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _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; } + FindParameters& setRange(Range _range) {range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } }; Artist() = default; Artist(const std::string& name, const std::optional& MBID = {}); // Accessors - static pointer getByMBID(Session& session, const UUID& MBID); - static pointer getById(Session& session, ArtistId id); - static bool exists(Session& session, ArtistId id); - static std::vector getByName(Session& session, const std::string& name); // exact match on name field - static std::vector getByClusters(Session& session, - const std::vector& clusters, // at least one track that belongs to these clusters - SortMethod sortMethod - ); - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, at least one artist that belongs to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords (name + sort name fields) - std::optional linkType, // if set, only artists that have produced at least one track with this link type - SortMethod sortMethod, - std::optional range, - bool& moreExpected); + static std::size_t getCount(Session& session); + static pointer find(Session& session, const UUID& MBID); + static pointer find(Session& session, ArtistId id); + static std::vector find(Session& session, const std::string& name); // exact match on name field + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findAllOrphans(Session& session, Range range); // No track related + static bool exists(Session& session, ArtistId id); - static std::vector getAll(Session& session); - static std::vector getAll(Session& session, SortMethod sortMethod); - static std::vector getAll(Session& session, SortMethod sortMethod, std::optional range, bool& moreResults); - static std::vector getAllIds(Session& session); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional linkType, std::optional size = {}); - static std::vector getAllOrphans(Session& session); // No track related - static std::vector getLastWritten(Session& session, - std::optional after, - const std::vector& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type - std::optional, - bool& moreResults); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - static std::vector getStarred(Session& session, - ObjectPtr user, - const std::vector& clusters, - std::optional linkType, // if set, only artists that have produced at least one track with this link type - SortMethod sortMethod, - std::optional, bool& moreResults); // Accessors const std::string& getName() const { return _name; } const std::string& getSortName() const { return _sortName; } std::optional getMBID() const { return UUID::fromString(_MBID); } - std::vector> getReleases(const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters + std::vector> getReleases(const std::vector& clusterIds = {}) const; // if non empty, get the releases that match all these clusters std::size_t getReleaseCount() const; std::vector> getTracks(std::optional linkType = {}) const; bool hasNonReleaseTracks(std::optional linkType = std::nullopt) const; - std::vector> getNonReleaseTracks(std::optional linkType, std::optional range, bool& moreResults) const; + RangeResults> getNonReleaseTracks(std::optional linkType, Range range) const; std::vector> getRandomTracks(std::optional count) const; // No artistLinkTypes means get them all - std::vector getSimilarArtists(EnumSet artistLinkTypes = {}, std::optional range = std::nullopt) const; + RangeResults findSimilarArtists(EnumSet artistLinkTypes = {}, Range range = {}) const; // Get the cluster of the tracks made by this artist // Each clusters are grouped by cluster type, sorted by the number of occurence @@ -127,7 +118,7 @@ class Artist : public Object Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade); } private: @@ -138,8 +129,8 @@ class Artist : public Object std::string _sortName; std::string _MBID; // Musicbrainz Identifier - Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist - Wt::Dbo::collection> _starringUsers; // Users that starred this artist + Wt::Dbo::collection> _trackArtistLinks; // Tracks involving this artist + Wt::Dbo::collection> _starredArtists; // starred entries for this artist }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/ArtistId.hpp b/src/libs/services/database/include/services/database/ArtistId.hpp new file mode 100644 index 00000000..01461d90 --- /dev/null +++ b/src/libs/services/database/include/services/database/ArtistId.hpp @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ArtistId) + diff --git a/src/libs/services/database/include/services/database/AuthToken.hpp b/src/libs/services/database/include/services/database/AuthToken.hpp new file mode 100644 index 00000000..46467dc0 --- /dev/null +++ b/src/libs/services/database/include/services/database/AuthToken.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2013 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 "services/database/AuthTokenId.hpp" +#include "services/database/Object.hpp" + +namespace Database +{ + class Session; + + class User; + class AuthToken : public Object + { + public: + AuthToken() = default; + AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user); + + // Utility + static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr user); + static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); + static pointer find(Session& session, std::string_view value); + + // Accessors + const Wt::WDateTime& getExpiry() const { return _expiry; } + ObjectPtr getUser() const { return _user; } + const std::string& getValue() const { return _value; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _value, "value"); + Wt::Dbo::field(a, _expiry, "expiry"); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + + std::string _value; + Wt::WDateTime _expiry; + Wt::Dbo::ptr _user; + }; +} // namespace Databas' + diff --git a/src/libs/services/database/include/services/database/AuthTokenId.hpp b/src/libs/services/database/include/services/database/AuthTokenId.hpp new file mode 100644 index 00000000..86de8770 --- /dev/null +++ b/src/libs/services/database/include/services/database/AuthTokenId.hpp @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(AuthTokenId) diff --git a/src/libs/services/database/include/services/database/Cluster.hpp b/src/libs/services/database/include/services/database/Cluster.hpp index 63032f75..59560f74 100644 --- a/src/libs/services/database/include/services/database/Cluster.hpp +++ b/src/libs/services/database/include/services/database/Cluster.hpp @@ -26,6 +26,9 @@ #include #include +#include "services/database/Object.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" namespace Database { @@ -42,20 +45,20 @@ class Cluster : public Object Cluster(ObjectPtr type, std::string_view name); // Find utility - static std::vector getAll(Session& session); - static std::vector getAllOrphans(Session& session); - static pointer getById(Session& session, ClusterId id); + static std::size_t getCount(Session& session); + static RangeResults find(Session& session, Range range); + static pointer find(Session& session, ClusterId id); + static RangeResults findOrphans(Session& session, Range range); // Create utility static pointer create(Session& session, ObjectPtr type, std::string_view name); // Accessors - const std::string& getName() const { return _name; } - ObjectPtr getType() const { return _clusterType; } - std::size_t getTracksCount() const { return _tracks.size(); } - std::vector> getTracks(std::optional offset = {}, std::optional limit = {}) const; - std::vector getTrackIds() const; - std::size_t getReleasesCount() const; + const std::string& getName() const { return _name; } + ObjectPtr getType() const { return _clusterType; } + std::size_t getTracksCount() const { return _tracks.size(); } + RangeResults getTracks(Range range) const; + std::size_t getReleasesCount() const; void addTrack(ObjectPtr track); @@ -85,19 +88,20 @@ class ClusterType : public Object ClusterType(std::string_view name); // Getters - static std::vector getAllOrphans(Session& session); - static std::vector getAllUsed(Session& session); - static pointer getByName(Session& session, const std::string& name); - static pointer getById(Session& session, ClusterTypeId id); - static std::vector getAll(Session& session); + static std::size_t getCount(Session& session); + static RangeResults find(Session& session, Range range); + static pointer find(Session& session, const std::string& name); + static pointer find(Session& session, ClusterTypeId id); + static RangeResults findOrphans(Session& session, Range range); + static RangeResults findUsed(Session& session, Range range); static pointer create(Session& session, const std::string& name); static void remove(Session& session, const std::string& name); // Accessors - const std::string& getName(void) const { return _name; } - std::vector getClusters() const; - Cluster::pointer getCluster(const std::string& name) const; + const std::string& getName() const { return _name; } + std::vector getClusters() const; + Cluster::pointer getCluster(const std::string& name) const; template void persist(Action& a) diff --git a/src/libs/services/database/include/services/database/ClusterId.hpp b/src/libs/services/database/include/services/database/ClusterId.hpp new file mode 100644 index 00000000..d14a88c0 --- /dev/null +++ b/src/libs/services/database/include/services/database/ClusterId.hpp @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ClusterId) +LMS_DECLARE_IDTYPE(ClusterTypeId) + diff --git a/src/libs/services/database/include/services/database/IdType.hpp b/src/libs/services/database/include/services/database/IdType.hpp new file mode 100644 index 00000000..04aaa2e4 --- /dev/null +++ b/src/libs/services/database/include/services/database/IdType.hpp @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +#pragma once + + +#include +#include +#include + +namespace Database +{ + class IdType + { + public: + using ValueType = Wt::Dbo::dbo_default_traits::IdType; + + IdType() = default; + IdType(ValueType id) : _id {id} { assert(isValid()); } + + bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } + std::string toString() const { assert(isValid()); return std::to_string(_id); } + + ValueType getValue() const { return _id; } + + bool operator==(IdType other) const { return other._id == _id; } + bool operator!=(IdType other) const { return !(*this == other); } + bool operator<(IdType other) const { return _id < other._id; } + bool operator>(IdType other) const { return _id > other._id; } + + private: + Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()}; + }; + +#define LMS_DECLARE_IDTYPE(name) \ + namespace Database { \ + class name : public IdType \ + { \ + public: \ + using IdType::IdType; \ + };\ + } \ + namespace std \ + { \ + template<> \ + class hash \ + { \ + public: \ + size_t operator()(Database::name id) const \ + { \ + return std::hash()(id.getValue()); \ + } \ + }; \ + } // ns std +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp new file mode 100644 index 00000000..32c02294 --- /dev/null +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -0,0 +1,111 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(ListenId) + +namespace Database +{ + +class Session; +class Track; +class User; + +class Listen : public Object +{ + public: + Listen() = default; + Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + + // Accessors + static std::size_t getCount(Session& session); + static pointer find(Session& session, ListenId id); + static RangeResults find(Session& session, UserId userId, Scrobbler scrobbler, Range = {}); + static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + + // Create + static pointer create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + + // Stats + static RangeResults getTopArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range = {}); + static RangeResults getTopReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + static RangeResults getTopTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + + static RangeResults getRecentArtists(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + std::optional linkType, + Range range = {}); + static RangeResults getRecentReleases(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + static RangeResults getRecentTracks(Session& session, + UserId userId, + Scrobbler scrobbler, + const std::vector& clusterIds, + Range range = {}); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _dateTime, "date_time"); + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Wt::WDateTime _dateTime; + Scrobbler _scrobbler; + Wt::Dbo::ptr _user; + Wt::Dbo::ptr _track; +}; + +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/Object.hpp b/src/libs/services/database/include/services/database/Object.hpp new file mode 100644 index 00000000..3c9f189f --- /dev/null +++ b/src/libs/services/database/include/services/database/Object.hpp @@ -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 . + */ + +#pragma once + +#include +#include "services/database/IdType.hpp" + +namespace Database +{ + template + class ObjectPtr + { + public: + ObjectPtr() = default; + ObjectPtr(Wt::Dbo::ptr obj) : _obj {obj} {} + + const T* operator->() const { return _obj.get(); } + operator bool() const { return _obj.get(); } + bool operator!() const { return !_obj.get(); } + + auto modify() { return _obj.modify(); } + void remove() { _obj.remove(); } + + private: + template friend class Object; + Wt::Dbo::ptr _obj; + }; + + template + class Object : public Wt::Dbo::Dbo + { + static_assert(std::is_base_of_v); + static_assert(!std::is_same_v); + + public: + using pointer = ObjectPtr; + using IdType = ObjectIdType; + + IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::template Dbo::id(); } + + // catch some misuses + typename Wt::Dbo::dbo_traits::IdType id() const = delete; + + protected: + // Can get raw dbo ptr only from Objects + template + static + Wt::Dbo::ptr getDboPtr(ObjectPtr ptr) { return ptr._obj; } + }; +} diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index 6af4e700..999899a6 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -25,7 +25,11 @@ #include #include +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "utils/UUID.hpp" namespace Database @@ -42,36 +46,42 @@ class User; class Release : public Object { public: + struct FindParameters + { + std::vector clusters; // if non empty, releases that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + ReleaseSortMethod sortMethod {ReleaseSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + std::optional dateRange; + UserId starringUser; // only releases starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } + FindParameters& setRange(Range _range) {range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; } + FindParameters& setDateRange(const std::optional& _dateRange) {dateRange = _dateRange; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + }; + Release() = default; Release(const std::string& name, const std::optional& MBID = {}); // Accessors - static std::size_t getCount(Session& session); - static pointer getByMBID(Session& session, const UUID& MBID); - static std::vector getByName(Session& session, const std::string& name); - static pointer getById(Session& session, ReleaseId id); - static bool exists(Session& session, ReleaseId id); - static std::vector getAllOrphans(Session& session); // no track related - static std::vector getAll(Session& session, std::optional range = std::nullopt); - static std::vector getAllIds(Session& session); - static std::vector getAllOrderedByArtist(Session& session, std::optional offset = {}, std::optional size = {}); - static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional size = {}); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional size = {}); - static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); - static std::vector getByYear(Session& session, int yearFrom, int yearTo, std::optional range = std::nullopt); - static std::vector getStarred(Session& session, ObjectPtr user, const std::vector& clusters, std::optional range, bool& moreResults); + static std::size_t getCount(Session& session); + static bool exists(Session& session, ReleaseId id); + static pointer find(Session& session, const UUID& MBID); + static std::vector find(Session& session, const std::string& name); + static pointer find(Session& session, ReleaseId id); + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findOrphans(Session& session, Range range); // no track related + static RangeResults findOrderedByArtist(Session& session, Range range); - static std::vector getByClusters(Session& session, const std::vector& clusters); - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, at least one release that belongs to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords - std::optional range, - bool& moreExpected); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - - std::vector> getTracks(const std::vector& clusters = {}) const; - std::size_t getTracksCount() const; - ObjectPtr getFirstTrack() const; + std::vector> getTracks(const std::vector& clusters = {}) const; + std::size_t getTracksCount() const; + ObjectPtr getFirstTrack() const; // Get the cluster of the tracks that belong to this release // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) @@ -110,7 +120,6 @@ class Release : public Object Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); } private: @@ -120,7 +129,6 @@ class Release : public Object std::string _MBID; Wt::Dbo::collection> _tracks; // Tracks in the release - Wt::Dbo::collection> _starringUsers; // Users that starred this release }; } // namespace Database diff --git a/src/libs/services/database/include/services/database/ReleaseId.hpp b/src/libs/services/database/include/services/database/ReleaseId.hpp new file mode 100644 index 00000000..c9c07c7a --- /dev/null +++ b/src/libs/services/database/include/services/database/ReleaseId.hpp @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(ReleaseId) + diff --git a/src/libs/services/database/include/services/database/ScanSettings.hpp b/src/libs/services/database/include/services/database/ScanSettings.hpp index 0a482956..75fa4a58 100644 --- a/src/libs/services/database/include/services/database/ScanSettings.hpp +++ b/src/libs/services/database/include/services/database/ScanSettings.hpp @@ -25,7 +25,10 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" + +LMS_DECLARE_IDTYPE(ScanSettingsId) namespace Database { diff --git a/src/libs/services/database/include/services/database/StarredArtist.hpp b/src/libs/services/database/include/services/database/StarredArtist.hpp new file mode 100644 index 00000000..0abb51e0 --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredArtist.hpp @@ -0,0 +1,81 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "services/database/ArtistId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredArtistId) + +namespace Database +{ + class Artist; + class Session; + class User; + + class StarredArtist : public Object + { + public: + StarredArtist() = default; + StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredArtistId id); + static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getArtist() const { return _artist; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _artist; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/StarredRelease.hpp b/src/libs/services/database/include/services/database/StarredRelease.hpp new file mode 100644 index 00000000..e7a1ad1e --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredRelease.hpp @@ -0,0 +1,81 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "services/database/ReleaseId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredReleaseId) + +namespace Database +{ + class Release; + class Session; + class User; + + class StarredRelease : public Object + { + public: + StarredRelease() = default; + StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredReleaseId id); + static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getRelease() const { return _release; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _release; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/StarredTrack.hpp b/src/libs/services/database/include/services/database/StarredTrack.hpp new file mode 100644 index 00000000..ab4cf1d1 --- /dev/null +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -0,0 +1,81 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "services/database/TrackId.hpp" +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/Session.hpp" +#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(StarredTrackId) + +namespace Database +{ + class Track; + class Session; + class User; + + class StarredTrack : public Object + { + public: + StarredTrack() = default; + StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + + // Search utility + static std::size_t getCount(Session& session); + static pointer find(Session& session, StarredTrackId id); + static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler); + + // Create utility + static pointer create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + + // Accessors + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } + Scrobbler getScrobbler() const { return _scrobbler; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _dateTime, "date_time"); + + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + Scrobbler _scrobbler; // for which scrobbler + Wt::WDateTime _dateTime; // when it was starred + + Wt::Dbo::ptr _track; + Wt::Dbo::ptr _user; + }; +} // namespace Database + diff --git a/src/libs/services/database/include/services/database/Track.hpp b/src/libs/services/database/include/services/database/Track.hpp index ccc9bb4e..38297cb3 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -34,7 +34,12 @@ #include "utils/EnumSet.hpp" #include "utils/UUID.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { @@ -44,50 +49,51 @@ class ClusterType; class Release; class Session; class TrackArtistLink; -class TrackFeatures; -class TrackListEntry; class TrackStats; class User; class Track : public Object { public: + struct FindParameters + { + std::vector clusters; // if non empty, tracks that belong to these clusters + std::vector keywords; // if non empty, name must match all of these keywords + TrackSortMethod sortMethod {TrackSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only tracks starred by this user + std::optional scrobbler; // and for this scrobbler + + FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } + FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } + FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } + FindParameters& setRange(Range _range) { range = _range; return *this; } + FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } + FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } + }; + struct PathResult + { + TrackId trackId; + std::filesystem::path path; + }; + Track() = default; Track(const std::filesystem::path& p); // Find utility functions - static std::size_t getCount(Session& session); - static pointer getByPath(Session& session, const std::filesystem::path& p); - static pointer getById(Session& session, TrackId id); - static bool exists(Session& session, TrackId id); - static std::vector getByRecordingMBID(Session& session, const UUID& MBID); - static std::vector getSimilarTracks(Session& session, - const std::vector& trackIds, - std::optional offset = {}, - std::optional size = {}); - static std::vector getByClusters(Session& session, - const std::vector& clusters); // tracks that belong to these clusters - static std::vector getByFilter(Session& session, - const std::vector& clusters, // if non empty, tracks that belong to these clusters - const std::vector& keywords, // if non empty, name must match all of these keywords - std::optional range, - bool& moreExpected); - static std::vector getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName); + static std::size_t getCount(Session& session); + static pointer findByPath(Session& session, const std::filesystem::path& p); + static pointer find(Session& session, TrackId id); + static bool exists(Session& session, TrackId id); + static std::vector findByRecordingMBID(Session& session, const UUID& MBID); + static RangeResults findSimilarTracks(Session& session, const std::vector& trackIds, Range range); - static std::vector getAll(Session& session, std::optional limit = std::nullopt); - static std::vector getAllRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); - static std::vector getAllIdsRandom(Session& session, const std::vector& clusters, std::optional limit = std::nullopt); - static std::vector getAllIds(Session& session); - static std::vector> getAllPaths(Session& session, std::optional offset = std::nullopt, std::optional size = std::nullopt); - static std::vector getMBIDDuplicates(Session& session); - static std::vector getLastWritten(Session& session, std::optional after, const std::vector& clusters, std::optional range, bool& moreResults); - static std::vector getAllWithRecordingMBIDAndMissingFeatures(Session& session); - static std::vector getAllIdsWithFeatures(Session& session, std::optional limit = {}); - static std::vector getAllIdsWithClusters(Session& session, std::optional limit = {}); - static std::vector getStarred(Session& session, - ObjectPtr user, - const std::vector& clusters, - std::optional range, bool& hasMore); + static RangeResults find(Session& session, const FindParameters& parameters); + static RangeResults findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName); + static RangeResults findPaths(Session& session, Range range); + static RangeResults findMBIDDuplicates(Session& session, Range range); + static RangeResults findWithRecordingMBIDAndMissingFeatures(Session& session, Range range); // Create utility static pointer create(Session& session, const std::filesystem::path& p); @@ -116,7 +122,6 @@ class Track : public Object void addArtistLink(const ObjectPtr& artistLink); void setRelease(ObjectPtr release) { _release = getDboPtr(release); } void setClusters(const std::vector>& clusters ); - void setFeatures(const ObjectPtr& features); std::size_t getScanVersion() const { return _scanVersion; } std::optional getTrackNumber() const; @@ -147,8 +152,6 @@ class Track : public Object ObjectPtr getRelease() const { return _release; } std::vector> getClusters() const; std::vector getClusterIds() const; - bool hasTrackFeatures() const; - ObjectPtr getTrackFeatures() const; std::vector>> getClusterGroups(const std::vector>& clusterTypes, std::size_t size) const; @@ -178,9 +181,6 @@ class Track : public Object Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _playlistEntries, Wt::Dbo::ManyToOne, "track"); - Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasOne(a, _trackFeatures); } private: @@ -215,10 +215,6 @@ class Track : public Object Wt::Dbo::ptr _release; Wt::Dbo::collection> _trackArtistLinks; Wt::Dbo::collection> _clusters; - Wt::Dbo::collection> _playlistEntries; - Wt::Dbo::collection> _starringUsers; - Wt::Dbo::weak_ptr _trackFeatures; - }; } // namespace database diff --git a/src/libs/services/database/include/services/database/TrackArtistLink.hpp b/src/libs/services/database/include/services/database/TrackArtistLink.hpp index 7708c69e..8a1ba0b2 100644 --- a/src/libs/services/database/include/services/database/TrackArtistLink.hpp +++ b/src/libs/services/database/include/services/database/TrackArtistLink.hpp @@ -23,9 +23,13 @@ #include +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" #include "services/database/Types.hpp" #include "utils/EnumSet.hpp" +LMS_DECLARE_IDTYPE(TrackArtistLinkId) + namespace Database { @@ -40,11 +44,10 @@ namespace Database TrackArtistLink(ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); static pointer create(Session& session, ObjectPtr track, ObjectPtr artist, TrackArtistLinkType type); - - static EnumSet getUsedTypes(Session& session); + static EnumSet findUsedTypes(Session& session); ObjectPtr getTrack() const { return _track; } - ObjectPtr getArtist() const { return _artist; } + ObjectPtr getArtist() const { return _artist; } TrackArtistLinkType getType() const { return _type; } template diff --git a/src/libs/services/database/include/services/database/TrackBookmark.hpp b/src/libs/services/database/include/services/database/TrackBookmark.hpp index 34ddb542..b5fb2985 100644 --- a/src/libs/services/database/include/services/database/TrackBookmark.hpp +++ b/src/libs/services/database/include/services/database/TrackBookmark.hpp @@ -23,7 +23,13 @@ #include +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" + +LMS_DECLARE_IDTYPE(TrackBookmarkId) namespace Database { @@ -41,10 +47,10 @@ class TrackBookmark : public Object static pointer create(Session& session, ObjectPtr user, ObjectPtr track); // Find utility functions - static std::vector getAll(Session& session); - static std::vector getByUser(Session& session, ObjectPtr user); - static pointer getByUser(Session& session, ObjectPtr user, ObjectPtr track); - static pointer getById(Session& session, TrackBookmarkId id); + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackBookmarkId id); + static RangeResults find(Session& session, UserId userId, Range range); + static pointer find(Session& session, UserId userId, TrackId trackId); // Setters void setOffset(std::chrono::milliseconds offset) { _offset = offset; } @@ -52,9 +58,9 @@ class TrackBookmark : public Object // Getters std::chrono::milliseconds getOffset() const { return _offset; } - std::string_view getComment() const { return _comment; } - ObjectPtr getTrack() const { return _track; } - ObjectPtr getUser() const { return _user; } + std::string_view getComment() const { return _comment; } + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } template void persist(Action& a) diff --git a/src/libs/services/database/include/services/database/TrackFeatures.hpp b/src/libs/services/database/include/services/database/TrackFeatures.hpp index fd251647..1c1251fa 100644 --- a/src/libs/services/database/include/services/database/TrackFeatures.hpp +++ b/src/libs/services/database/include/services/database/TrackFeatures.hpp @@ -26,8 +26,13 @@ #include +#include "services/database/IdType.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" +LMS_DECLARE_IDTYPE(TrackFeaturesId) + namespace Database { class Session; @@ -43,12 +48,21 @@ class TrackFeatures : public Object TrackFeatures() = default; TrackFeatures(ObjectPtr track, const std::string& jsonEncodedFeatures); + // Find utilities + static std::size_t getCount(Session& session); + static pointer find(Session& session, TrackFeaturesId id); + static pointer find(Session& session, TrackId trackId); + static RangeResults find(Session& session, Range range); + // Create utility - static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); + static pointer create(Session& session, ObjectPtr track, const std::string& jsonEncodedFeatures); FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set& featureNames) const; + // Accessors + Wt::Dbo::ptr getTrack() const { return _track; } + template void persist(Action& a) { @@ -57,7 +71,6 @@ class TrackFeatures : public Object } private: - std::string _data; Wt::Dbo::ptr _track; }; diff --git a/src/libs/services/database/include/services/database/TrackId.hpp b/src/libs/services/database/include/services/database/TrackId.hpp new file mode 100644 index 00000000..40684e3e --- /dev/null +++ b/src/libs/services/database/include/services/database/TrackId.hpp @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(TrackId) + diff --git a/src/libs/services/database/include/services/database/TrackList.hpp b/src/libs/services/database/include/services/database/TrackList.hpp index 48f56c23..b168ad5d 100644 --- a/src/libs/services/database/include/services/database/TrackList.hpp +++ b/src/libs/services/database/include/services/database/TrackList.hpp @@ -21,13 +21,18 @@ #include #include -#include +#include #include #include #include +#include "services/database/ClusterId.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/TrackListId.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { @@ -52,25 +57,25 @@ class TrackList : public Object TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr user); // Stats utility - std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopArtists(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getTopReleases(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTopTracks(const std::vector& clusterIds, std::optional range, bool& moreResults) const; // Search utility - static pointer get(Session& session, std::string_view name, Type type, ObjectPtr user); - static pointer getById(Session& session, TrackListId tracklistId); - static std::vector getAll(Session& session); - static std::vector getAll(Session& session, ObjectPtr user); - static std::vector getAll(Session& session, ObjectPtr user, Type type); + static std::size_t getCount(Session& session); + static pointer find(Session& session, std::string_view name, Type type, UserId userId); + static pointer find(Session& session, TrackListId tracklistId); + static RangeResults find(Session& session, UserId userId, Range range); + static RangeResults find(Session& session, UserId userId, Type type, Range range); // Create utility static pointer create(Session& session, std::string_view name, Type type, bool isPublic, ObjectPtr user); // Accessors - std::string getName() const { return _name; } - bool isPublic() const { return _isPublic; } - Type getType() const { return _type; } - ObjectPtr getUser() const { return _user; } + std::string_view getName() const { return _name; } + bool isPublic() const { return _isPublic; } + Type getType() const { return _type; } + ObjectPtr getUser() const { return _user; } // Modifiers void setName(const std::string& name) { _name = name; } @@ -81,18 +86,20 @@ class TrackList : public Object bool isEmpty() const; std::size_t getCount() const; ObjectPtr getEntry(std::size_t pos) const; - std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; + std::vector> getEntries(std::optional offset = {}, std::optional size = {}) const; ObjectPtr getEntryByTrackAndDateTime(ObjectPtr track, const Wt::WDateTime& dateTime) const; - // Get track bya + std::vector> getArtists(const std::vector& clusters, std::optional linkType, ArtistSortMethod sortMethod, std::optional range, bool& moreResults) const; + std::vector> getReleases(const std::vector& clusters, std::optional range, bool& moreResults) const; + std::vector> getTracks(const std::vector& clusters, std::optional range, bool& moreResults) const; - std::vector> getArtistsReverse(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; - std::vector> getReleasesReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector> getTracksReverse(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + // Sorted by date time + std::vector> getArtistsOrderedByRecentFirst(const std::vector& clusterIds, std::optional linkType, std::optional range, bool& moreResults) const; + std::vector> getReleasesOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; + std::vector> getTracksOrderedByRecentFirst(const std::vector& clusterIds, std::optional range, bool& moreResults) const; - std::vector getTrackIds() const; - - std::chrono::milliseconds getDuration() const; + std::vector getTrackIds() const; + std::chrono::milliseconds getDuration() const; // Get clusters, order by occurence std::vector> getClusters() const; @@ -129,12 +136,13 @@ class TrackListEntry : public Object public: TrackListEntry() = default; TrackListEntry(ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime); + TrackListEntry(ObjectPtr track, ObjectPtr tracklist); // find utility static pointer getById(Session& session, TrackListEntryId id); // Create utility - static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime()); + static pointer create(Session& session, ObjectPtr track, ObjectPtr tracklist, const Wt::WDateTime& dateTime = {}); // Accessors ObjectPtr getTrack() const { return _track; } @@ -151,7 +159,7 @@ class TrackListEntry : public Object private: - Wt::WDateTime _dateTime; + Wt::WDateTime _dateTime; // optional date time Wt::Dbo::ptr _track; Wt::Dbo::ptr _tracklist; }; diff --git a/src/libs/services/database/include/services/database/TrackListId.hpp b/src/libs/services/database/include/services/database/TrackListId.hpp new file mode 100644 index 00000000..3dc35d07 --- /dev/null +++ b/src/libs/services/database/include/services/database/TrackListId.hpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 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 "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(TrackListId) +LMS_DECLARE_IDTYPE(TrackListEntryId) + diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index c2fb4146..c1c0021b 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -22,63 +22,126 @@ #include #include #include -#include +#include namespace Database { - class IdType - { - public: - using ValueType = Wt::Dbo::dbo_default_traits::IdType; - - IdType() = default; - IdType(ValueType id) : _id {id} { assert(isValid()); } - - bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } - std::string toString() const { assert(isValid()); return std::to_string(_id); } - - ValueType getValue() const { return _id; } - - bool operator==(IdType other) const { return other._id == _id; } - bool operator!=(IdType other) const { return !(*this == other); } - bool operator<(IdType other) const { return other._id < _id; } - - private: - Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()}; - }; + // Caution: do not change enum values if they are set! + // Request: + // size = 0 => no size limit! + // Response (via RangeResults) + // size => results size struct Range { std::size_t offset {}; - std::size_t limit {}; + std::size_t size {}; + + // TODO remove this + operator bool() const { return size != 0; } }; + template + struct RangeResults + { + Range range; + std::vector results; + bool moreResults; + + RangeResults getSubRange(Range subRange) + { + assert(subRange.offset >= range.offset); + + if (!subRange.size) + subRange.size = range.size - (subRange.offset - range.offset); + + subRange.offset = std::min(subRange.offset, range.offset + range.size); + subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); + + RangeResults subResults; + + auto itBegin {std::cbegin(results) + subRange.offset - range.offset}; + auto itEnd {itBegin + subRange.size}; + subResults.results.reserve(std::distance(itBegin, itEnd)); + std::copy(itBegin, itEnd, std::back_inserter(subResults.results)); + + subResults.range = subRange; + if (subRange.offset + subRange.size == range.offset + range.size) + subResults.moreResults = moreResults; + else + subResults.moreResults = true; + + return subResults; + } + }; + + struct DateRange + { + Wt::WDate begin; + Wt::WDate end; + + static DateRange fromYearRange(int from, int to); + }; + + enum class ArtistSortMethod + { + None, + ByName, + BySortName, + Random, + LastWritten, + StarredDateDesc, + }; + + enum class ReleaseSortMethod + { + None, + Name, + Date, + Random, + LastWritten, + StarredDateDesc, + }; + + enum class TrackSortMethod + { + None, + Random, + LastWritten, + StarredDateDesc, + }; + + // Do not change enum values! enum class TrackArtistLinkType { - Artist, // regular artist - Arranger, - Composer, - Conductor, - Lyricist, - Mixer, - Performer, - Producer, - ReleaseArtist, - Remixer, - Writer, + Artist = 0, // regular track artist + Arranger = 1, + Composer = 2, + Conductor = 3, + Lyricist = 4, + Mixer = 5, + Performer = 6, + Producer = 7, + ReleaseArtist = 8, + Remixer = 9, + Writer = 10, }; // User selectable audio file formats // Do not change values enum class AudioFormat { - MP3 = 1, - OGG_OPUS = 2, - OGG_VORBIS = 3, - WEBM_VORBIS = 4, + MP3 = 1, + OGG_OPUS = 2, + OGG_VORBIS = 3, + WEBM_VORBIS = 4, MATROSKA_OPUS = 5, }; + using Bitrate = std::uint32_t; + // Do not remove values! + void visitAllowedAudioBitrates(std::function); + bool isAudioBitrateAllowed(Bitrate bitrate); // Do not change enum values! enum class Scrobbler @@ -95,82 +158,19 @@ namespace Database DEMO = 2, }; - template - class ObjectPtr + // Do not change enum values! + enum class UITheme { - public: - ObjectPtr() = default; - ObjectPtr(Wt::Dbo::ptr obj) : _obj {obj} {} - - const T* operator->() const { return _obj.get(); } - operator bool() const { return _obj.get(); } - bool operator!() const { return !_obj.get(); } - - auto modify() { return _obj.modify(); } - void remove() { _obj.remove(); } - - private: - template friend class Object; - Wt::Dbo::ptr _obj; + Light = 0, + Dark = 1, }; - template - class Object : public Wt::Dbo::Dbo + // Do not change enum values! + enum class SubsonicArtistListMode { - static_assert(std::is_base_of_v); - static_assert(!std::is_same_v); - - public: - using pointer = ObjectPtr; - using IdType = ObjectIdType; - - IdType getId() const { return Wt::Dbo::Dbo::self()->Wt::Dbo::template Dbo::id(); } - - // catch some misuses - typename Wt::Dbo::dbo_traits::IdType id() const = delete; - - protected: - // Can get raw dbo ptr only from Objects - template - static - Wt::Dbo::ptr getDboPtr(ObjectPtr ptr) { return ptr._obj; } + AllArtists = 0, + ReleaseArtists = 1, + TrackArtists = 2, }; } -// TODO factorize hash with std::enable_if -#define LMS_DECLARE_IDTYPE(name) \ -namespace Database { \ - class name : public IdType \ - { \ - public: \ - using IdType::IdType; \ - };\ -} \ -namespace std \ -{ \ - template<> \ - class hash \ - { \ - public: \ - size_t operator()(Database::name id) const \ - { \ - return std::hash()(id.getValue()); \ - } \ - }; \ -} // ns std - -LMS_DECLARE_IDTYPE(ArtistId) -LMS_DECLARE_IDTYPE(AuthTokenId) -LMS_DECLARE_IDTYPE(ClusterId) -LMS_DECLARE_IDTYPE(ClusterTypeId) -LMS_DECLARE_IDTYPE(ReleaseId) -LMS_DECLARE_IDTYPE(ScanSettingsId) -LMS_DECLARE_IDTYPE(TrackArtistLinkId) -LMS_DECLARE_IDTYPE(TrackBookmarkId) -LMS_DECLARE_IDTYPE(TrackFeaturesId) -LMS_DECLARE_IDTYPE(TrackId) -LMS_DECLARE_IDTYPE(TrackListId) -LMS_DECLARE_IDTYPE(TrackListEntryId) -LMS_DECLARE_IDTYPE(UserId) - - diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index 34bfac2c..e42a2181 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -26,51 +26,15 @@ #include #include +#include "services/database/Object.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "utils/UUID.hpp" namespace Database { - -class Artist; -class Release; +class AuthToken; class Session; -class TrackList; -class Track; - -class User; -class AuthToken : public Object -{ - public: - AuthToken() = default; - AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr user); - - // Utility - static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, ObjectPtr user); - static void removeExpiredTokens(Session& session, const Wt::WDateTime& now); - static pointer getByValue(Session& session, const std::string& value); - static pointer getById(Session& session, AuthTokenId tokenId); - - // Accessors - const Wt::WDateTime& getExpiry() const { return _expiry; } - ObjectPtr getUser() const { return _user; } - const std::string& getValue() const { return _value; } - - template - void persist(Action& a) - { - Wt::Dbo::field(a, _value, "value"); - Wt::Dbo::field(a, _expiry, "expiry"); - Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); - } - - private: - - std::string _value; - Wt::WDateTime _expiry; - - Wt::Dbo::ptr _user; -}; class User : public Object { @@ -81,35 +45,10 @@ class User : public Object std::string hash; }; - // Do not change enum values! - enum class UITheme - { - Light = 0, - Dark = 1, - }; - - // Do not remove values! - static inline const std::set audioTranscodeAllowedBitrates - { - 64000, - 96000, - 128000, - 192000, - 320000, - }; - - // Do not change enum values! - enum class SubsonicArtistListMode - { - AllArtists = 0, - ReleaseArtists = 1, - TrackArtists = 2, - }; - static inline const std::size_t MinNameLength {3}; static inline const std::size_t MaxNameLength {15}; static inline const bool defaultSubsonicTranscodeEnable {true}; - static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS}; + static inline const AudioFormat defaultSubsonicTranscodeFormat {AudioFormat::OGG_OPUS}; static inline const Bitrate defaultSubsonicTranscodeBitrate {128000}; static inline const UITheme defaultUITheme {UITheme::Dark}; static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists}; @@ -121,18 +60,17 @@ class User : public Object // utility static pointer create(Session& session, std::string_view loginName); - static pointer getById(Session& session, UserId id); - static pointer getByLoginName(Session& session, std::string_view loginName); - static std::vector getAll(Session& session); - static std::vector getAllIds(Session& session); - static pointer getDemo(Session& session); - static std::size_t getCount(Session& session); + static std::size_t getCount(Session& session); + static pointer find(Session& session, UserId id); + static pointer find(Session& session, std::string_view loginName); + static RangeResults find(Session& session, Range range); + static pointer findDemoUser(Session& session); // accessors - const std::string& getLoginName() const { return _loginName; } - PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; } - Wt::WDateTime getLastLogin() const { return _lastLogin; } - std::size_t getAuthTokensCount() const { return _authTokens.size(); } + const std::string& getLoginName() const { return _loginName; } + PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; } + const Wt::WDateTime& getLastLogin() const { return _lastLogin; } + std::size_t getAuthTokensCount() const { return _authTokens.size(); } // write void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } @@ -165,21 +103,6 @@ class User : public Object Scrobbler getScrobbler() const { return _scrobbler; } std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } - ObjectPtr getQueuedTrackList(Session& session) const; - - void star(ObjectPtr artist); - void unstar(ObjectPtr artist); - bool isStarred(ObjectPtr artist) const; - - void star(ObjectPtr release); - void unstar(ObjectPtr release); - bool isStarred(ObjectPtr release) const; - - // Stars - void star(ObjectPtr track); - void unstar(ObjectPtr track); - bool isStarred(ObjectPtr track) const; - template void persist(Action& a) { @@ -196,20 +119,15 @@ class User : public Object Wt::Dbo::field(a, _scrobbler, "scrobbler"); Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); - // UI settings + // UI player settings Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); Wt::Dbo::field(a, _repeatAll, "repeat_all"); Wt::Dbo::field(a, _radio, "radio"); - Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user"); - Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); } private: - std::string _loginName; std::string _passwordSalt; std::string _passwordHash; @@ -228,16 +146,11 @@ class User : public Object int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; // User's dynamic data (UI) - int _curPlayingTrackPos {}; // Current track position in queue + int _curPlayingTrackPos {}; // Current track position in queue bool _repeatAll {}; bool _radio {}; - Wt::Dbo::collection> _tracklists; - Wt::Dbo::collection> _starredArtists; - Wt::Dbo::collection> _starredReleases; - Wt::Dbo::collection> _starredTracks; Wt::Dbo::collection> _authTokens; - }; } // namespace Databas' diff --git a/src/libs/services/database/include/services/database/UserId.hpp b/src/libs/services/database/include/services/database/UserId.hpp new file mode 100644 index 00000000..c337f8fa --- /dev/null +++ b/src/libs/services/database/include/services/database/UserId.hpp @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/IdType.hpp" + +LMS_DECLARE_IDTYPE(UserId) + diff --git a/src/libs/services/database/test/Artist.cpp b/src/libs/services/database/test/Artist.cpp index d94c6daf..81dd3771 100644 --- a/src/libs/services/database/test/Artist.cpp +++ b/src/libs/services/database/test/Artist.cpp @@ -21,13 +21,14 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleArtist) +TEST_F(DatabaseFixture, Artist) { { auto transaction {session.createSharedTransaction()}; EXPECT_FALSE(Artist::exists(session, 35)); EXPECT_FALSE(Artist::exists(session, 0)); EXPECT_FALSE(Artist::exists(session, 1)); + EXPECT_EQ(Artist::getCount(session), 0); } ScopedArtist artist {session, "MyArtist"}; @@ -40,22 +41,23 @@ TEST_F(DatabaseFixture, SingleArtist) EXPECT_EQ(artist.get()->getId(), artist.getId()); EXPECT_TRUE(Artist::exists(session, artist.getId())); + EXPECT_EQ(Artist::getCount(session), 1); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getAll(session, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getAllOrphans(session); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::findAllOrphans(session, Range {}); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } -TEST_F(DatabaseFixture, SingleTrackSingleArtist) +TEST_F(DatabaseFixture, Artist_singleTrack) { ScopedTrack track {session, "MyTrack"}; ScopedArtist artist {session, "MyArtist"}; @@ -68,7 +70,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -102,7 +104,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtist) } } -TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) +TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) { ScopedTrack track {session, "MyTrack"}; ScopedArtist artist {session, "MyArtist"}; @@ -116,17 +118,16 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - bool hasMore{}; - EXPECT_EQ(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Artist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::ReleaseArtist, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_EQ(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Writer, Artist::SortMethod::ByName, std::nullopt, hasMore).size(), 1); - EXPECT_TRUE(Artist::getByFilter(session, {}, {}, TrackArtistLinkType::Composer, Artist::SortMethod::ByName, std::nullopt, hasMore).empty()); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); } { @@ -149,7 +150,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRoles) } } -TEST_F(DatabaseFixture,SingleTrackMultiArtists) +TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) { ScopedTrack track {session, "track"}; ScopedArtist artist1 {session, "artist1"}; @@ -165,7 +166,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -179,8 +180,8 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({TrackArtistLinkType::Artist}).size(), 2); EXPECT_TRUE(track->getArtists({TrackArtistLinkType::ReleaseArtist}).empty()); - EXPECT_EQ(Artist::getAll(session, Artist::SortMethod::ByName).size(), 2); - EXPECT_EQ(Artist::getAllIds(session).size(), 2); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}).results.size(), 2); + EXPECT_EQ(Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); } { @@ -196,7 +197,7 @@ TEST_F(DatabaseFixture,SingleTrackMultiArtists) } } -TEST_F(DatabaseFixture, SingleArtistSearchByName) +TEST_F(DatabaseFixture, Artist_findByName) { ScopedArtist artist {session, "AAA"}; ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans @@ -210,22 +211,22 @@ TEST_F(DatabaseFixture, SingleArtistSearchByName) { auto transaction {session.createSharedTransaction()}; - bool more {}; - EXPECT_TRUE(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setKeywords({"N"})).results.empty()); - const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artistsByAAA.size(), 1); - EXPECT_EQ(artistsByAAA.front()->getId(), artist.getId()); + const auto artistsByAAA {Artist::find(session, Artist::FindParameters {}.setKeywords({"A"}))}; + ASSERT_EQ(artistsByAAA.results.size(), 1); + EXPECT_EQ(artistsByAAA.results.front(), artist.getId()); - const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artistsByZZZ.size(), 1); - EXPECT_EQ(artistsByZZZ.front()->getId(), artist.getId()); + const auto artistsByZZZ {Artist::Artist::find(session, Artist::FindParameters {}.setKeywords({"Z"}))}; + ASSERT_EQ(artistsByZZZ.results.size(), 1); + EXPECT_EQ(artistsByZZZ.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByName(session, "NNN").empty()); + EXPECT_TRUE(Artist::find(session, "NNN").empty()); + EXPECT_EQ(Artist::find(session, "AAA").size(), 1); } } -TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) +TEST_F(DatabaseFixture, Artist_findByNameEscaped) { ScopedArtist artist1 {session, "MyArtist%"}; ScopedArtist artist2 {session, "%MyArtist"}; @@ -238,70 +239,55 @@ TEST_F(DatabaseFixture, MultipleArtistsSearchByNameEscaped) { auto transaction {session.createSharedTransaction()}; { - const auto artists {Artist::getByName(session, "MyArtist%")}; + const auto artists {Artist::find(session, "MyArtist%")}; ASSERT_TRUE(artists.size() == 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - EXPECT_TRUE(Artist::getByName(session, "MyArtistFoo").empty()); + EXPECT_TRUE(Artist::find(session, "MyArtistFoo").empty()); } { - const auto artists {Artist::getByName(session, "%MyArtist")}; + const auto artists {Artist::find(session, "%MyArtist")}; ASSERT_TRUE(artists.size() == 1); EXPECT_EQ(artists.front()->getId(), artist2.getId()); - EXPECT_TRUE(Artist::getByName(session, "FooMyArtist").empty()); + EXPECT_TRUE(Artist::find(session, "FooMyArtist").empty()); } { - const auto artists {Artist::getByName(session, "%_MyArtist")}; + const auto artists {Artist::find(session, "%_MyArtist")}; ASSERT_TRUE(artists.size() == 1); ASSERT_EQ(artists.front()->getId(), artist3.getId()); - EXPECT_TRUE(Artist::getByName(session, "%CMyArtist").empty()); + EXPECT_TRUE(Artist::find(session, "%CMyArtist").empty()); } } - // get by filter only works with tracks links... - ScopedTrack track {session, "MyTrack"}; // filters does not work on orphans - - { - auto transaction {session.createUniqueTransaction()}; - - TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist3.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist4.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist5.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track.get(), artist6.get(), TrackArtistLinkType::Artist); - } - { auto transaction {session.createSharedTransaction()}; - bool more; { - const auto artists {Artist::getByFilter(session, {}, {"MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - EXPECT_EQ(artists.size(), 6); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist"}))}; + EXPECT_EQ(artists.results.size(), 6); } { - const auto artists {Artist::getByFilter(session, {}, {"MyArtist%"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist4.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist1.getId()); + EXPECT_EQ(artists.results[1], artist4.getId()); } { - const auto artists {Artist::getByFilter(session, {}, {"%MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist5.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2.getId()); + EXPECT_EQ(artists.results[1], artist5.getId()); } { - const auto artists {Artist::getByFilter(session, {}, {"_MyArtist"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, more)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists[0]->getId(), artist3.getId()); + const auto artists {Artist::find(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist3.getId()); } } } -TEST_F(DatabaseFixture, MultiArtistsSortMethod) +TEST_F(DatabaseFixture, Artist_sortMethod) { ScopedArtist artistA {session, "artistA"}; ScopedArtist artistB {session, "artistB"}; @@ -316,20 +302,20 @@ TEST_F(DatabaseFixture, MultiArtistsSortMethod) { auto transaction {session.createSharedTransaction()}; - auto allArtistsByName {Artist::getAll(session, Artist::SortMethod::ByName)}; - auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)}; + auto allArtistsByName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName))}; + auto allArtistsBySortName {Artist::find(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; - ASSERT_EQ(allArtistsByName.size(), 2); - EXPECT_EQ(allArtistsByName.front()->getId(), artistA.getId()); - EXPECT_EQ(allArtistsByName.back()->getId(), artistB.getId()); + ASSERT_EQ(allArtistsByName.results.size(), 2); + EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); + EXPECT_EQ(allArtistsByName.results.back(), artistB.getId()); - ASSERT_EQ(allArtistsBySortName.size(), 2); - EXPECT_EQ(allArtistsBySortName.front()->getId(), artistB.getId()); - EXPECT_EQ(allArtistsBySortName.back()->getId(), artistA.getId()); + ASSERT_EQ(allArtistsBySortName.results.size(), 2); + EXPECT_EQ(allArtistsBySortName.results.front(), artistB.getId()); + EXPECT_EQ(allArtistsBySortName.results.back(), artistA.getId()); } } -TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) +TEST_F(DatabaseFixture, Artist_nonReleaseTracks) { ScopedArtist artist {session, "artist"}; ScopedTrack track1 {session, "MyTrack1"}; @@ -340,9 +326,8 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) auto transaction {session.createSharedTransaction()}; EXPECT_FALSE(artist->hasNonReleaseTracks(std::nullopt)); - bool moreResults; - const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; - EXPECT_EQ(tracks.size(), 0); + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})}; + EXPECT_EQ(tracks.results.size(), 0); } { @@ -358,44 +343,11 @@ TEST_F(DatabaseFixture, SingleArtistNonReleaseTracks) { auto transaction {session.createSharedTransaction()}; - bool moreResults; - const auto tracks {artist->getNonReleaseTracks(std::nullopt, std::nullopt, moreResults )}; + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {})}; EXPECT_TRUE(artist->hasNonReleaseTracks(std::nullopt)); - EXPECT_FALSE(moreResults); - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleStarredArtist) -{ - ScopedArtist artist {session, "MyArtist"}; - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - - { - auto transaction {session.createUniqueTransaction()}; - - EXPECT_FALSE(user->isStarred(artist.get())); - } - - { - auto transaction {session.createUniqueTransaction()}; - - auto trackArtistLink {TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist)}; - user.get().modify()->star(artist.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_TRUE(user->isStarred(artist.get())); - - bool hasMore {}; - auto artists {Artist::getStarred(session, user.get(), {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, hasMore)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); - EXPECT_FALSE(hasMore); + EXPECT_FALSE(tracks.moreResults); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front()->getId(), track2.getId()); } } diff --git a/src/libs/services/database/test/CMakeLists.txt b/src/libs/services/database/test/CMakeLists.txt index 605c3af5..99bbf7b7 100644 --- a/src/libs/services/database/test/CMakeLists.txt +++ b/src/libs/services/database/test/CMakeLists.txt @@ -2,9 +2,17 @@ add_executable(test-database Artist.cpp Cluster.cpp + Common.cpp DatabaseTest.cpp + Listen.cpp Release.cpp + StarredArtist.cpp + StarredRelease.cpp + StarredTrack.cpp Track.cpp + TrackBookmark.cpp + TrackFeatures.cpp + TrackList.cpp ) target_link_libraries(test-database PRIVATE diff --git a/src/libs/services/database/test/Cluster.cpp b/src/libs/services/database/test/Cluster.cpp index aba28a41..df6c677d 100644 --- a/src/libs/services/database/test/Cluster.cpp +++ b/src/libs/services/database/test/Cluster.cpp @@ -24,60 +24,73 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleCluster) +TEST_F(DatabaseFixture, Cluster) { + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + } + ScopedClusterType clusterType {session, "MyType"}; + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(ClusterType::getCount(session), 1); + } + { ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; { auto transaction {session.createUniqueTransaction()}; - auto clusters {Cluster::getAll(session)}; - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster.getId()); - EXPECT_EQ(clusters.front()->getType()->getId(), clusterType.getId()); + EXPECT_EQ(Cluster::getCount(session), 1); + EXPECT_EQ(cluster->getType()->getId(), clusterType.getId()); - clusters = Cluster::getAllOrphans(session); - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster.getId()); + auto clusters {Cluster::find(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster.getId()); - auto clusterTypes {ClusterType::getAll(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + clusters = Cluster::findOrphans(session, Range {}); + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster.getId()); - clusterTypes = ClusterType::getAllUsed(session); - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + auto clusterTypes {ClusterType::find(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - clusterTypes = ClusterType::getAllOrphans(session); - EXPECT_TRUE(clusterTypes.empty()); + clusterTypes = ClusterType::findUsed(session, Range {}); + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); + + clusterTypes = ClusterType::findOrphans(session, Range {}); + EXPECT_TRUE(clusterTypes.results.empty()); } } { auto transaction {session.createUniqueTransaction()}; - auto clusterTypes {ClusterType::getAllOrphans(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + auto clusterTypes {ClusterType::findOrphans(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); - ASSERT_TRUE(ClusterType::getAllUsed(session).empty()); + ASSERT_TRUE(ClusterType::findUsed(session, Range {}).results.empty()); } } -TEST_F(DatabaseFixture, SingleTrackSingleCluster) +TEST_F(DatabaseFixture, Cluster_singleTrack) { ScopedTrack track {session, "MyTrack"}; ScopedClusterType clusterType {session, "MyClusterType"}; { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - auto clusterTypes {ClusterType::getAllOrphans(session)}; - ASSERT_EQ(clusterTypes.size(), 1); - EXPECT_EQ(clusterTypes.front()->getId(), clusterType.getId()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + auto clusterTypes {ClusterType::findOrphans(session, Range {})}; + ASSERT_EQ(clusterTypes.results.size(), 1); + EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); } ScopedCluster cluster1 {session, clusterType.lockAndGet(), "MyCluster1"}; @@ -85,17 +98,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto clusters {Cluster::getAllOrphans(session)}; - EXPECT_EQ(clusters.size(), 2); + auto clusters {Cluster::findOrphans(session, Range {})}; + EXPECT_EQ(clusters.results.size(), 2); EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusterIds().empty()); } - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Track::getAllIdsWithClusters(session).empty()); - } - { auto transaction {session.createUniqueTransaction()}; @@ -104,29 +112,22 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::getAllIdsWithClusters(session)}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front(), track.getId()); - } + auto clusters {Cluster::findOrphans(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), cluster2.getId()); - { - auto transaction {session.createSharedTransaction()}; - auto clusters {Cluster::getAllOrphans(session)}; - ASSERT_EQ(clusters.size(), 1); - EXPECT_EQ(clusters.front()->getId(), cluster2.getId()); - - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto tracks {Track::getByClusters(session, {cluster1.getId()})}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); + auto tracks {Track::find(session, Track::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); - tracks = Track::getByClusters(session, {cluster2.getId()}); - EXPECT_TRUE(tracks.empty()); + tracks = Track::find(session, Track::FindParameters {}.setClusters({cluster2.getId()})); + EXPECT_TRUE(tracks.results.empty()); } { @@ -142,7 +143,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleCluster) } } -TEST_F(DatabaseFixture, MultipleTracksSingleCluster) +TEST_F(DatabaseFixture, Cluster_multiTracks) { std::list tracks; ScopedClusterType clusterType {session, "MyClusterType"}; @@ -160,19 +161,19 @@ TEST_F(DatabaseFixture, MultipleTracksSingleCluster) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); EXPECT_EQ(cluster->getTracksCount(), tracks.size()); - for (auto trackCluster : cluster->getTracks()) + for (TrackId trackId : cluster->getTracks(Range {}).results) { - auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackCluster->getId() == track.getId(); })}; + auto it {std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); })}; EXPECT_TRUE(it != std::cend(tracks)); } } } -TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) +TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease) { ScopedClusterType clusterType {session, "ClusterType"}; ScopedCluster cluster1 {session, clusterType.lockAndGet(), "Cluster1"}; @@ -268,16 +269,25 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersTopRelease) } } -TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) +TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) { ScopedTrack track {session, "MyTrackFile"}; ScopedRelease release {session, "MyRelease"}; - ScopedClusterType clusterType {session, "MyClusterType"}; - ScopedCluster cluster {session, clusterType .lockAndGet(), "MyCluster"}; { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllIdsWithClusters(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + } + + ScopedClusterType clusterType {session, "MyClusterType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedCluster unusedCluster {session, clusterType.lockAndGet(), "MyClusterUnused"}; + + { + auto transaction {session.createSharedTransaction()}; + ASSERT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2); + EXPECT_TRUE(Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})).results.empty()); + EXPECT_EQ(Release::find(session, Release::FindParameters {}).results.size(), 1); } { @@ -289,24 +299,27 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getAllIdsWithClusters(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front(), release.getId()); + + { + auto clusters {Cluster::findOrphans(session, Range {})}; + ASSERT_EQ(clusters.results.size(), 1); + EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); + } } { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); + auto releases {Release::find(session, Release::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); } { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getByClusters(session, {cluster.getId()})}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + auto releases {Release::find(session, Release::FindParameters {}.setClusters({unusedCluster.getId()}))}; + EXPECT_EQ(releases.results.size(), 0); } { @@ -314,6 +327,8 @@ TEST_F(DatabaseFixture,SingleTrackSingleReleaseSingleCluster) EXPECT_EQ(cluster->getReleasesCount(), 1); EXPECT_EQ(cluster->getTracksCount(), 1); + EXPECT_EQ(unusedCluster->getReleasesCount(), 0); + EXPECT_EQ(unusedCluster->getTracksCount(), 0); } } @@ -334,10 +349,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); - EXPECT_EQ(Cluster::getAllOrphans(session).size(), 2); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); + EXPECT_EQ(Cluster::findOrphans(session, Range {}).results.size(), 2); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -349,12 +364,12 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName).empty()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()})).results.empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty()); cluster2.get().modify()->addTrack(track.get()); } @@ -362,19 +377,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster2.getId()})); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()}, Artist::SortMethod::ByName); - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + artists = Artist::find(session, Artist::FindParameters {}.setClusters({cluster1.getId()})); + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); - EXPECT_TRUE(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty()); + EXPECT_TRUE(Artist::find(session, Artist::FindParameters {}.setClusters({cluster3.getId()})).results.empty()); } } @@ -395,17 +410,17 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } @@ -435,8 +450,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -445,9 +460,9 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters) std::vector clusterIds; std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); }); - auto artists {Artist::getByClusters(session, clusterIds, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters(clusterIds))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); } } @@ -471,15 +486,11 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity) { auto transaction {session.createSharedTransaction()}; - const auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; - EXPECT_EQ(similarTracks.size(), tracks.size() - 1); - for (auto similarTrack : similarTracks) + const auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})}; + EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); + for (const TrackId similarTrackId : similarTracks.results) { - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), - [&](const ScopedTrack& track) - { - return similarTrack->getId() == track.getId(); - }) != std::cend(tracks)); + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } } } @@ -516,17 +527,17 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity) auto transaction {session.createSharedTransaction()}; { - auto similarTracks {Track::getSimilarTracks(session, {tracks.back().getId()}, 0, 4)}; - EXPECT_EQ(similarTracks.size(), 4); - for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); + auto similarTracks {Track::findSimilarTracks(session, {tracks.back().getId()}, Range {0, 4})}; + EXPECT_EQ(similarTracks.results.size(), 4); + for (const TrackId similarTrackId : similarTracks.results) + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 5), std::next(std::cend(tracks), -1), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } { - auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; - EXPECT_EQ(similarTracks.size(), tracks.size() - 1); - for (auto similarTrack : similarTracks) - EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack->getId() == track.getId(); }) != std::cend(tracks)); + auto similarTracks {Track::findSimilarTracks(session, {tracks.front().getId()}, Range {})}; + EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); + for (const TrackId similarTrackId : similarTracks.results) + EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); } } } @@ -539,11 +550,6 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) ScopedClusterType clusterType {session, "MyType"}; ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; - { - auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Artist::getAllIdsWithClusters(session).empty()); - } - { auto transaction {session.createUniqueTransaction()}; @@ -555,25 +561,18 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Cluster::getAllOrphans(session).empty()); - EXPECT_TRUE(ClusterType::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - } - - { - auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getAllIdsWithClusters(session)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front(), artist.getId()); + EXPECT_TRUE(Cluster::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(ClusterType::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); } { auto transaction {session.createSharedTransaction()}; - auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist.getId()); + auto artists {Artist::find(session, Artist::FindParameters {}.setClusters({cluster.getId()}))}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist.getId()); auto releases {artist->getReleases()}; ASSERT_EQ(releases.size(), 1); @@ -735,9 +734,9 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); } { @@ -750,15 +749,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size() , 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -767,15 +766,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -784,15 +783,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -801,7 +800,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); } @@ -810,7 +809,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); } @@ -819,11 +818,11 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getArtistsReverse({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty()); } { @@ -836,17 +835,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist2.getId()); EXPECT_EQ(artists[1]->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release2.getId()); EXPECT_EQ(releases[1]->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track2.getId()); EXPECT_EQ(tracks[1]->getId(),track1.getId()); @@ -856,17 +855,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist2.getId()); EXPECT_EQ(artists[1]->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release2.getId()); EXPECT_EQ(releases[1]->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track2.getId()); EXPECT_EQ(tracks[1]->getId(), track1.getId()); @@ -876,15 +875,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist1.getId()); - const auto releases {trackList->getReleasesReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release1.getId()); - const auto tracks {trackList->getTracksReverse({cluster1.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track1.getId()); } @@ -893,15 +892,15 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists.front()->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({cluster2.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 1); EXPECT_EQ(releases.front()->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({cluster2.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 1); EXPECT_EQ(tracks.front()->getId(), track2.getId()); } @@ -916,17 +915,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist1.getId()); EXPECT_EQ(artists[1]->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release1.getId()); EXPECT_EQ(releases[1]->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track1.getId()); EXPECT_EQ(tracks[1]->getId(), track2.getId()); @@ -936,17 +935,17 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed) auto transaction {session.createSharedTransaction()}; bool moreResults {}; - const auto artists {trackList->getArtistsReverse({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults)}; ASSERT_EQ(artists.size(), 2); EXPECT_EQ(artists[0]->getId(), artist1.getId()); EXPECT_EQ(artists[1]->getId(), artist2.getId()); - const auto releases {trackList->getReleasesReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto releases {trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(releases.size(), 2); EXPECT_EQ(releases[0]->getId(), release1.getId()); EXPECT_EQ(releases[1]->getId(), release2.getId()); - const auto tracks {trackList->getTracksReverse({cluster3.getId()}, std::nullopt, moreResults)}; + const auto tracks {trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults)}; ASSERT_EQ(tracks.size(), 2); EXPECT_EQ(tracks[0]->getId(), track1.getId()); EXPECT_EQ(tracks[1]->getId(), track2.getId()); @@ -964,9 +963,9 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(artist1->getSimilarArtists().empty()); - EXPECT_TRUE(artist2->getSimilarArtists().empty()); - EXPECT_TRUE(artist3->getSimilarArtists().empty()); + EXPECT_TRUE(artist1->findSimilarArtists().results.empty()); + EXPECT_TRUE(artist2->findSimilarArtists().results.empty()); + EXPECT_TRUE(artist3->findSimilarArtists().results.empty()); } std::list tracks; @@ -998,38 +997,38 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) auto transaction {session.createSharedTransaction()}; { - auto artists {artist1->getSimilarArtists()}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists()}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::ReleaseArtist})}; - EXPECT_EQ(artists.empty(), 1); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::ReleaseArtist})}; + EXPECT_EQ(artists.results.empty(), 1); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist2.getId()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results.front(), artist2.getId()); } { - auto artists {artist1->getSimilarArtists({TrackArtistLinkType::Composer})}; - EXPECT_TRUE(artists.empty()); + auto artists {artist1->findSimilarArtists({TrackArtistLinkType::Composer})}; + EXPECT_TRUE(artists.results.empty()); } { - auto artists {artist2->getSimilarArtists()}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist3.getId()); + auto artists {artist2->findSimilarArtists()}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist1.getId()); + EXPECT_EQ(artists.results[1], artist3.getId()); } } } diff --git a/src/libs/services/database/test/Common.cpp b/src/libs/services/database/test/Common.cpp new file mode 100644 index 00000000..033aa754 --- /dev/null +++ b/src/libs/services/database/test/Common.cpp @@ -0,0 +1,211 @@ +/* + * 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 "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Release.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" +#include "services/database/Track.hpp" +#include "services/database/TrackArtistLink.hpp" +#include "services/database/TrackBookmark.hpp" +#include "services/database/TrackList.hpp" +#include "services/database/Types.hpp" +#include "services/database/User.hpp" + +TmpDatabase::TmpDatabase() +: _tmpFile {std::tmpnam(nullptr)} +, _fileDeleter {_tmpFile} +, _db {_tmpFile} +{ +} + +Database::Db& +TmpDatabase::getDb() +{ + return _db; +} + +DatabaseFixture::~DatabaseFixture() +{ + testDatabaseEmpty(); +} + +void +DatabaseFixture::SetUpTestCase() +{ + _tmpDb = std::make_unique(); + { + Database::Session s {_tmpDb->getDb()}; + s.prepareTables(); + s.optimize(); + + // remove default created entries + { + auto transaction {s.createUniqueTransaction()}; + auto clusterTypes {Database::ClusterType::find(s, Database::Range {})}; + for (auto clusterTypeId : clusterTypes.results) + { + auto clusterType {Database::ClusterType::find(s, clusterTypeId)}; + clusterType.remove(); + } + } + } +} + +void +DatabaseFixture::TearDownTestCase() +{ + _tmpDb.reset(); +} + +void +DatabaseFixture::testDatabaseEmpty() +{ + using namespace Database; + + auto uniqueTransaction {session.createUniqueTransaction()}; + + EXPECT_EQ(Artist::getCount(session), 0); + EXPECT_EQ(Cluster::getCount(session), 0); + EXPECT_EQ(ClusterType::getCount(session), 0); + EXPECT_EQ(Listen::getCount(session), 0); + EXPECT_EQ(Release::getCount(session), 0); + EXPECT_EQ(StarredArtist::getCount(session), 0); + EXPECT_EQ(StarredRelease::getCount(session), 0); + EXPECT_EQ(StarredTrack::getCount(session), 0); + EXPECT_EQ(Track::getCount(session), 0); + EXPECT_EQ(TrackBookmark::getCount(session), 0); + EXPECT_EQ(TrackList::getCount(session), 0); + EXPECT_EQ(User::getCount(session), 0); +} + +TEST_F(DatabaseFixture, Common_subRangeEmpty) +{ + using namespace Database; + + RangeResults results; + results.range = Range {0, 0}; + results.results = {}; + results.moreResults = false; + + { + auto subRange {results.getSubRange(Range {0, 0})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + EXPECT_EQ(subRange.range, Range {}); + } + { + auto subRange {results.getSubRange(Range {0, 1})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Common_IdType) +{ + using namespace Database; + + { + const IdType id{}; + EXPECT_FALSE(id.isValid()); + } + + { + const IdType id{0}; + EXPECT_TRUE(id.isValid()); + } + + { + const IdType id1{0}; + const IdType id2{0}; + EXPECT_EQ(id1, id2); + } + + { + const IdType id1{0}; + const IdType id2{1}; + EXPECT_NE(id1, id2); + EXPECT_LT(id1, id2); + EXPECT_GT(id2, id1); + } +} + +TEST_F(DatabaseFixture, Common_subRange) +{ + using namespace Database; + + RangeResults results; + results.range = Range {0, 2}; + results.results = {5, 6}; + results.moreResults = false; + + { + auto subRange {results.getSubRange(Range {0, 1})}; + EXPECT_TRUE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 5); + } + { + auto subRange {results.getSubRange(Range {1, 1})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + } + { + auto subRange {results.getSubRange(Range {0, 2})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + } + { + auto subRange {results.getSubRange(Range {})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 2); + EXPECT_EQ(subRange.results.front(), 5); + EXPECT_EQ(subRange.results.back(), 6); + EXPECT_EQ(subRange.range, results.range); + } + + { + auto subRange {results.getSubRange(Range {1, 0})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 1); + EXPECT_EQ(subRange.results.front(), 6); + const Range expectedRange {1, 1}; + EXPECT_EQ(subRange.range, expectedRange); + } + { + auto subRange {results.getSubRange(Range {3, 2})}; + EXPECT_FALSE(subRange.moreResults); + ASSERT_EQ(subRange.results.size(), 0); + const Range expectedRange {2, 0}; + EXPECT_EQ(subRange.range, expectedRange); + } +} + + + diff --git a/src/libs/services/database/test/Common.hpp b/src/libs/services/database/test/Common.hpp index 75aa654d..25c47de6 100644 --- a/src/libs/services/database/test/Common.hpp +++ b/src/libs/services/database/test/Common.hpp @@ -27,11 +27,13 @@ #include "services/database/Artist.hpp" #include "services/database/Cluster.hpp" #include "services/database/Db.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" #include "services/database/TrackBookmark.hpp" +#include "services/database/TrackFeatures.hpp" #include "services/database/TrackList.hpp" #include "services/database/Types.hpp" #include "services/database/User.hpp" @@ -57,7 +59,7 @@ class ScopedEntity { auto transaction {_session.createUniqueTransaction()}; - auto entity {T::getById(_session, _id)}; + auto entity {T::find(_session, _id)}; entity.remove(); } @@ -76,7 +78,7 @@ class ScopedEntity { _session.checkSharedLocked(); - auto entity {T::getById(_session, _id)}; + auto entity {T::find(_session, _id)}; EXPECT_TRUE(entity); return entity; } @@ -98,7 +100,6 @@ using ScopedCluster = ScopedEntity; using ScopedClusterType = ScopedEntity; using ScopedRelease = ScopedEntity; using ScopedTrack = ScopedEntity; -using ScopedTrackBookmark = ScopedEntity; using ScopedTrackList = ScopedEntity; using ScopedUser = ScopedEntity; @@ -120,60 +121,27 @@ class ScopedFileDeleter final class TmpDatabase final { public: - Database::Db& getDb() { return _db; } + TmpDatabase (); + + Database::Db& getDb(); private: - const std::filesystem::path _tmpFile {std::tmpnam(nullptr)}; - ScopedFileDeleter fileDeleter {_tmpFile}; - Database::Db _db {_tmpFile}; + const std::filesystem::path _tmpFile; + ScopedFileDeleter _fileDeleter; + Database::Db _db; }; class DatabaseFixture : public ::testing::Test { public: - ~DatabaseFixture() - { - testDatabaseEmpty(); - } + ~DatabaseFixture(); public: - static void SetUpTestCase() - { - _tmpDb = std::make_unique(); - { - Database::Session s {_tmpDb->getDb()}; - s.prepareTables(); - s.optimize(); - - // remove default created entries - { - auto transaction {s.createUniqueTransaction()}; - auto clusterTypes {Database::ClusterType::getAll(s)}; - for (auto& clusterType : clusterTypes) - clusterType.remove(); - } - } - } - - static void TearDownTestCase() - { - _tmpDb.reset(); - } + static void SetUpTestCase(); + static void TearDownTestCase(); private: - void testDatabaseEmpty() - { - auto uniqueTransaction {session.createUniqueTransaction()}; - - EXPECT_TRUE(Database::Artist::getAll(session, Database::Artist::SortMethod::ByName).empty()); - EXPECT_TRUE(Database::Cluster::getAll(session).empty()); - EXPECT_TRUE(Database::ClusterType::getAll(session).empty()); - EXPECT_TRUE(Database::Release::getAll(session).empty()); - EXPECT_TRUE(Database::Track::getAll(session).empty()); - EXPECT_TRUE(Database::TrackBookmark::getAll(session).empty()); - EXPECT_TRUE(Database::TrackList::getAll(session).empty()); - EXPECT_TRUE(Database::User::getAll(session).empty()); - } + void testDatabaseEmpty(); static inline std::unique_ptr _tmpDb {}; diff --git a/src/libs/services/database/test/DatabaseTest.cpp b/src/libs/services/database/test/DatabaseTest.cpp index 2e0a386a..f106e06a 100644 --- a/src/libs/services/database/test/DatabaseTest.cpp +++ b/src/libs/services/database/test/DatabaseTest.cpp @@ -42,8 +42,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllOrphans(session).empty()); - EXPECT_TRUE(Artist::getAllOrphans(session).empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); + EXPECT_TRUE(Artist::findAllOrphans(session, Range {}).results.empty()); } { @@ -89,8 +89,8 @@ TEST_F(DatabaseFixture, SingleUser) { { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(User::getAll(session).empty()); - EXPECT_TRUE(User::getAllIds(session).empty()); + EXPECT_TRUE(User::find(session, Range {}).results.empty()); + EXPECT_EQ(User::getCount(session), 0); } ScopedUser user {session, "MyUser"}; @@ -98,214 +98,8 @@ TEST_F(DatabaseFixture, SingleUser) { auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(user->getQueuedTrackList(session)->getCount(), 0); - EXPECT_EQ(User::getAll(session).size(), 1); - EXPECT_EQ(User::getAllIds(session).size(), 1); - } -} - -TEST_F(DatabaseFixture, SingleTrackList) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; - - { - auto transaction {session.createSharedTransaction()}; - - auto trackLists {TrackList::getAll(session, user.get(), TrackList::Type::Playlist)}; - ASSERT_EQ(trackLists.size(), 1); - EXPECT_EQ(trackLists.front()->getId(), trackList.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; - std::list tracks; - - for (std::size_t i {}; i < 10; ++i) - { - tracks.emplace_back(session, "MyTrack" + std::to_string(i)); - - auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, tracks.back().get(), trackList.get()); - } - - { - auto transaction {session.createSharedTransaction()}; - - ASSERT_EQ(trackList->getCount(), tracks.size()); - const auto trackIds {trackList->getTrackIds()}; - for (auto trackId : trackIds) - EXPECT_TRUE(std::any_of(std::cbegin(tracks), std::cend(tracks), [trackId](const ScopedTrack& track) { return track.getId() == trackId; })); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedTrack track3 {session, "MyTrack3"}; - - { - Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - auto transaction {session.createUniqueTransaction()}; - TrackListEntry::create(session, track1.get(), trackList.get(), now); - TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(-1)); - TrackListEntry::create(session, track3.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults; - const auto tracks {trackList.get()->getTracksReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 3); - EXPECT_EQ(tracks.front()->getId(), track3.getId()); - EXPECT_EQ(tracks.back()->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) -{ - ScopedUser user {session, "MyUser"}; - ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack1"}; - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - ScopedRelease release1 {session, "MyRelease1"}; - ScopedRelease release2 {session, "MyRelease2"}; - - const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; - - { - auto transaction {session.createUniqueTransaction()}; - - track1.get().modify()->setRelease(release1.get()); - track2.get().modify()->setRelease(release2.get()); - TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); - TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); - } - { - - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - EXPECT_TRUE(trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getReleasesReverse({}, std::nullopt, moreResults).empty()); - EXPECT_TRUE(trackList->getTracksReverse({}, std::nullopt, moreResults).empty()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - TrackListEntry::create(session, track1.get(), trackList.get(), now); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 1); - EXPECT_EQ(artists.front()->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); - - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; - EXPECT_EQ(tracks.size(), 1); - } - - { - auto transaction {session.createUniqueTransaction()}; - - TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist2.getId()); - EXPECT_EQ(artists[1]->getId(), artist1.getId()); - - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release1.getId()); - - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track1.getId()); - } - - { - auto transaction {session.createUniqueTransaction()}; - - TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2)); - } - - { - auto transaction {session.createSharedTransaction()}; - - bool moreResults {}; - const auto artists {trackList->getArtistsReverse({}, std::nullopt, std::nullopt, moreResults)}; - ASSERT_EQ(artists.size(), 2); - EXPECT_EQ(artists[0]->getId(), artist1.getId()); - EXPECT_EQ(artists[1]->getId(), artist2.getId()); - - const auto releases {trackList->getReleasesReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release1.getId()); - EXPECT_EQ(releases[1]->getId(), release2.getId()); - - const auto tracks {trackList->getTracksReverse({}, std::nullopt, moreResults)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track1.getId()); - EXPECT_EQ(tracks[1]->getId(), track2.getId()); - } -} - -TEST_F(DatabaseFixture, SingleTrackSingleUserSingleBookmark) -{ - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()}; - - { - auto transaction {session.createUniqueTransaction()}; - - bookmark.get().modify()->setComment("MyComment"); - bookmark.get().modify()->setOffset(std::chrono::milliseconds {5}); - } - - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_EQ(TrackBookmark::getAll(session).size(), 1); - - const auto bookmarks {TrackBookmark::getByUser(session, user.get())}; - ASSERT_EQ(bookmarks.size(), 1); - EXPECT_EQ(bookmarks.back(), bookmark.get()); - } - { - auto transaction {session.createSharedTransaction()}; - - auto userBookmark {TrackBookmark::getByUser(session, user.get(), track.get())}; - ASSERT_TRUE(userBookmark); - EXPECT_EQ(userBookmark, bookmark.get()); - - EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5}); - EXPECT_EQ(userBookmark->getComment(), "MyComment"); + EXPECT_EQ(User::find(session, Range {}).results.size(), 1); + EXPECT_EQ(User::getCount(session), 1); } } diff --git a/src/libs/services/database/test/Listen.cpp b/src/libs/services/database/test/Listen.cpp new file mode 100644 index 00000000..1b5b12b9 --- /dev/null +++ b/src/libs/services/database/test/Listen.cpp @@ -0,0 +1,892 @@ +/* + * 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 "services/database/Listen.hpp" + +using namespace Database; + +using ScopedListen = ScopedEntity; + +TEST_F(DatabaseFixture, Listen_getAll) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 1); + } + + { + auto transaction {session.createUniqueTransaction()}; + ScopedListen listen2 {session, user.get(), track.get(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}}}; + + EXPECT_EQ(Listen::getCount(session), 2); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(Listen::getCount(session), 1); + } +} + +TEST_F(DatabaseFixture, Listen_get) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto listens {Listen::find(session, user->getId(), Scrobbler::ListenBrainz)}; + EXPECT_EQ(listens.results.size(), 0); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto listens {Listen::find(session, user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(listens.moreResults, false); + ASSERT_EQ(listens.results.size(), 1); + EXPECT_EQ(listens.results.front()->getId(), listen->getId()); + } +} + +TEST_F(DatabaseFixture, Listen_get_multi) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedListen listen3 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}}}; + ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto listens {Listen::find(session, user->getId(), Scrobbler::Internal)}; + ASSERT_EQ(listens.results.size(), 3); + EXPECT_EQ(listens.results[0]->getId(), listen1.getId()); + EXPECT_EQ(listens.results[1]->getId(), listen2.getId()); + EXPECT_EQ(listens.results[2]->getId(), listen3.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_get_byDateTime) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + const Wt::WDateTime dateTime2 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2}}; + ASSERT_GT(dateTime2, dateTime1); + + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1}; + ScopedListen listen2 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2}; + + { + auto transaction {session.createSharedTransaction()}; + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime1)}; + ASSERT_TRUE(listen); + EXPECT_EQ(listen->getId(), listen1.getId()); + } + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2)}; + ASSERT_TRUE(listen); + EXPECT_EQ(listen->getId(), listen2.getId()); + } + + { + Listen::pointer listen {Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2.addSecs(56))}; + EXPECT_FALSE(listen); + } + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists) +{ + ScopedTrack track1 {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime1 {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime1}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + EXPECT_EQ(artists.moreResults, false); + } + + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime1.addSecs(1)}; + + { + auto transaction {session.createUniqueTransaction()}; + + TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)}; + EXPECT_EQ(artists.results.size(), 0); + } + { + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists_multi) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist2 {session, "MyArtist2"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2->getId()); + EXPECT_EQ(artists.results[1], artist1->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.moreResults, true); + EXPECT_EQ(artists.results[0], artist2->getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedRelease release {session, "MyRelease"}; + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases_multi) +{ + ScopedTrack track1 {session, "MyTrack"}; + ScopedTrack track2 {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + { + auto transaction {session.createSharedTransaction()}; + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release1.getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release1.getId()); + EXPECT_EQ(releases.results[1], release2.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTracks) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 0); + } + + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(tracks.moreResults, false); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTrack_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track1.getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedListen listen5 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track1.getId()); + EXPECT_EQ(tracks.results[1], track2.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getTopTracks_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + 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(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist->getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::ListenBrainz, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer)}; + EXPECT_EQ(artists.results.size(), 0); + } + { + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedArtist artist2 {session, "MyArtist2"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + EXPECT_EQ(artists.results.size(), 0); + } + + ScopedListen listen1 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist1->getId()); + } + ScopedListen listen2 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], artist2->getId()); + EXPECT_EQ(artists.results[1], artist1->getId()); + } + ScopedListen listen3 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1})}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.moreResults, true); + EXPECT_EQ(artists.results[0], artist2->getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedArtist artist {session, "MyArtist"}; + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt)}; + ASSERT_EQ(artists.results.size(), 1); + EXPECT_EQ(artists.results[0], artist.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(releases.moreResults, false); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + { + auto transaction {session.createSharedTransaction()}; + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release2.getId()); + } + + ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release1.getId()); + EXPECT_EQ(releases.results[1], release2.getId()); + } + + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } + + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(releases.moreResults, false); + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release1.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + track.get().modify()->setRelease(release.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 0); + } + + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 0); + } + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::ListenBrainz, {})}; + EXPECT_EQ(tracks.moreResults, false); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks_multi) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen1 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track2.getId()); + } + + ScopedListen listen2 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track1.getId()); + EXPECT_EQ(tracks.results[1], track2.getId()); + } + + ScopedListen listen3 {session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(2)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } + + ScopedListen listen4 {session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(-1)}; + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {})}; + EXPECT_EQ(tracks.moreResults, false); + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track1.getId()); + } +} + +TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + const Wt::WDateTime dateTime {Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1}}; + ScopedListen listen {session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime}; + ScopedClusterType clusterType {session, "MyType"}; + ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 0); + } + { + auto transaction {session.createUniqueTransaction()}; + cluster.get().modify()->addTrack(track.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()})}; + EXPECT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results[0], track.getId()); + } +} + diff --git a/src/libs/services/database/test/Release.cpp b/src/libs/services/database/test/Release.cpp index 84f765f4..9ff1db24 100644 --- a/src/libs/services/database/test/Release.cpp +++ b/src/libs/services/database/test/Release.cpp @@ -21,11 +21,12 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleRelease) +TEST_F(DatabaseFixture, Release) { { auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(Release::getCount(session), 0); EXPECT_FALSE(Release::exists(session, 0)); EXPECT_FALSE(Release::exists(session, 1)); } @@ -35,20 +36,23 @@ TEST_F(DatabaseFixture, SingleRelease) { auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(Release::getCount(session), 1); EXPECT_TRUE(Release::exists(session, release.getId())); - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + auto releases {Release::findOrphans(session, Range {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); - releases = Release::getAll(session); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); + { + auto releases {Release::find(session, Release::FindParameters {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); + EXPECT_EQ(release->getDuration(), std::chrono::seconds {0}); + } } } -TEST_F(DatabaseFixture, SingleTrackSingleRelease) +TEST_F(DatabaseFixture, Release_singleTrack) { ScopedRelease release {session, "MyRelease"}; @@ -64,7 +68,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) { auto transaction {session.createSharedTransaction()}; - EXPECT_TRUE(Release::getAllOrphans(session).empty()); + EXPECT_TRUE(Release::findOrphans(session, Range {}).results.empty()); EXPECT_EQ(release->getTracksCount(), 1); ASSERT_EQ(release->getTracks().size(), 1); @@ -80,19 +84,19 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseName")}; + ASSERT_EQ(tracks.results.size(), 1); + EXPECT_EQ(tracks.results.front(), track.getId()); } { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; - EXPECT_EQ(tracks.size(), 0); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackName", "MyReleaseFoo")}; + EXPECT_EQ(tracks.results.size(), 0); } { auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::getByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; - EXPECT_EQ(tracks.size(), 0); + auto tracks {Track::findByNameAndReleaseName(session, "MyTrackFoo", "MyReleaseName")}; + EXPECT_EQ(tracks.results.size(), 0); } } @@ -101,9 +105,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleRelease) EXPECT_TRUE(release->getTracks().empty()); - auto releases {Release::getAllOrphans(session)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); + auto releases {Release::findOrphans(session, Range {})}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release.getId()); } } @@ -138,38 +142,37 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName) { auto transaction {session.createSharedTransaction()}; - bool more; { - const auto releases {Release::getByFilter(session, {}, {"Release"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 6); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Release"}))}; + EXPECT_EQ(releases.results.size(), 6); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease"}, std::nullopt, more)}; - EXPECT_EQ(releases.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(releases), std::cend(releases), [&](const Release::pointer& release) { return release->getId() == release6.getId(); })); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(releases.results), std::cend(releases.results), [&](const ReleaseId releaseId) { return releaseId == release6.getId(); })); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release2.getId()); - EXPECT_EQ(releases[1]->getId(), release4.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%"}))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release2.getId()); + EXPECT_EQ(releases.results[1], release4.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 2); - EXPECT_EQ(releases[0]->getId(), release3.getId()); - EXPECT_EQ(releases[1]->getId(), release5.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"%MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], release3.getId()); + EXPECT_EQ(releases.results[1], release5.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"Foo%MyRelease"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release5.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"Foo%MyRelease"}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release5.getId()); } { - const auto releases {Release::getByFilter(session, {}, {"MyRelease%Foo"}, std::nullopt, more)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases[0]->getId(), release4.getId()); + const auto releases {Release::find(session, Release::FindParameters {}.setKeywords({"MyRelease%Foo"}))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results[0], release4.getId()); } } } @@ -323,8 +326,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) { auto transaction {session.createSharedTransaction()}; - const auto releases {Release::getByYear(session, 0, 3000)}; - EXPECT_EQ(releases.size(), 0); + const auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000)))}; + EXPECT_EQ(releases.results.size(), 0); } { @@ -348,48 +351,48 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate) { auto transaction {session.createSharedTransaction()}; - auto releases {Release::getByYear(session, 1950, 2000)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); + auto releases {Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000)))}; + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::getByYear(session, 1994, 1994); - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release1.getId()); + releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1994, 1994))); + ASSERT_EQ(releases.results.size(), 1); + EXPECT_EQ(releases.results.front(), release1.getId()); - releases = Release::getByYear(session, 1993, 1993); - ASSERT_EQ(releases.size(), 0); + releases = Release::find(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1993, 1993))); + ASSERT_EQ(releases.results.size(), 0); } } -TEST_F(DatabaseFixture, SingleStarredRelease) +TEST_F(DatabaseFixture, Release_writtenAfter) { ScopedRelease release {session, "MyRelease"}; ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - { - auto transaction {session.createSharedTransaction()}; - - EXPECT_FALSE(user->isStarred(release.get())); - } + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; { auto transaction {session.createUniqueTransaction()}; - + track.get().modify()->setLastWriteTime(dateTime); track.get().modify()->setRelease(release.get()); - user.get().modify()->star(release.get()); } { auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + } - EXPECT_TRUE(user->isStarred(release.get())); + { + auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; + EXPECT_EQ(releases.results.size(), 1); + } - bool hasMore {}; - auto releases {Release::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; - ASSERT_EQ(releases.size(), 1); - EXPECT_EQ(releases.front()->getId(), release.getId()); - EXPECT_FALSE(hasMore); + { + auto transaction {session.createSharedTransaction()}; + const auto releases {Release::find(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; + EXPECT_EQ(releases.results.size(), 0); } } diff --git a/src/libs/services/database/test/StarredArtist.cpp b/src/libs/services/database/test/StarredArtist.cpp new file mode 100644 index 00000000..22f11f8e --- /dev/null +++ b/src/libs/services/database/test/StarredArtist.cpp @@ -0,0 +1,109 @@ +/* + * 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 "services/database/StarredArtist.hpp" + +using namespace Database; + +using ScopedStarredArtist = ScopedEntity; + +TEST_F(DatabaseFixture, StarredArtist) +{ + ScopedArtist artist {session, "MyArtist"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredArtist); + EXPECT_EQ(StarredArtist::getCount(session), 0); + + auto artists {Artist::find(session, Artist::FindParameters {})}; + EXPECT_EQ(artists.results.size(), 1); + } + + ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotArtist->getId(), starredArtist->getId()); + EXPECT_EQ(StarredArtist::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Artist::find(session, Artist::FindParameters {})}; + EXPECT_EQ(artists.results.size(), 1); + + artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(artists.results.size(), 1); + + artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(artists.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredArtist_dateTime) +{ + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedArtist artist2 {session, "MyArtist2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(artists.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredArtist1.get().modify()->setDateTime(dateTime); + starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ArtistSortMethod::StarredDateDesc))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId()); + EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredArtist1.get().modify()->setDateTime(dateTime); + starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ArtistSortMethod::StarredDateDesc))}; + ASSERT_EQ(artists.results.size(), 2); + EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId()); + EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId()); + } +} diff --git a/src/libs/services/database/test/StarredRelease.cpp b/src/libs/services/database/test/StarredRelease.cpp new file mode 100644 index 00000000..2059a785 --- /dev/null +++ b/src/libs/services/database/test/StarredRelease.cpp @@ -0,0 +1,109 @@ +/* + * 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 "services/database/StarredRelease.hpp" + +using namespace Database; + +using ScopedStarredRelease = ScopedEntity; + +TEST_F(DatabaseFixture, StarredRelease) +{ + ScopedRelease release {session, "MyRelease"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredRelease); + EXPECT_EQ(StarredRelease::getCount(session), 0); + + auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + } + + ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotRelease->getId(), starredRelease->getId()); + EXPECT_EQ(StarredRelease::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::find(session, Release::FindParameters {})}; + EXPECT_EQ(releases.results.size(), 1); + + releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(releases.results.size(), 1); + + releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(releases.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredRelease_dateTime) +{ + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(releases.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredRelease1.get().modify()->setDateTime(dateTime); + starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ReleaseSortMethod::StarredDateDesc))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId()); + EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredRelease1.get().modify()->setDateTime(dateTime); + starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(ReleaseSortMethod::StarredDateDesc))}; + ASSERT_EQ(releases.results.size(), 2); + EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId()); + EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId()); + } +} diff --git a/src/libs/services/database/test/StarredTrack.cpp b/src/libs/services/database/test/StarredTrack.cpp new file mode 100644 index 00000000..6bd00cb0 --- /dev/null +++ b/src/libs/services/database/test/StarredTrack.cpp @@ -0,0 +1,109 @@ +/* + * 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 "services/database/StarredTrack.hpp" + +using namespace Database; + +using ScopedStarredTrack = ScopedEntity; + +TEST_F(DatabaseFixture, StarredTrack) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; + + { + auto transaction {session.createSharedTransaction()}; + + auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_FALSE(starredTrack); + EXPECT_EQ(StarredTrack::getCount(session), 0); + + auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); + } + + ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + { + auto transaction {session.createSharedTransaction()}; + + auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)}; + EXPECT_EQ(gotTrack->getId(), starredTrack->getId()); + EXPECT_EQ(StarredTrack::getCount(session), 1); + } + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); + + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); + EXPECT_EQ(tracks.results.size(), 1); + + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); + EXPECT_EQ(tracks.results.size(), 0); + } +} + +TEST_F(DatabaseFixture, StarredTrack_dateTime) +{ + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedUser user {session, "MyUser"}; + + ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + + { + auto transaction {session.createSharedTransaction()}; + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; + EXPECT_EQ(tracks.results.size(), 2); + } + + { + auto transaction {session.createUniqueTransaction()}; + + starredTrack1.get().modify()->setDateTime(dateTime); + starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(TrackSortMethod::StarredDateDesc))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId()); + EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId()); + } + { + auto transaction {session.createUniqueTransaction()}; + + starredTrack1.get().modify()->setDateTime(dateTime); + starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1)); + + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal) + .setSortMethod(TrackSortMethod::StarredDateDesc))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId()); + EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId()); + } +} diff --git a/src/libs/services/database/test/Track.cpp b/src/libs/services/database/test/Track.cpp index a4b22d61..3868225d 100644 --- a/src/libs/services/database/test/Track.cpp +++ b/src/libs/services/database/test/Track.cpp @@ -23,10 +23,11 @@ using namespace Database; -TEST_F(DatabaseFixture, SingleTrack) +TEST_F(DatabaseFixture, Track) { { auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 0); EXPECT_EQ(Track::getCount(session), 0); EXPECT_FALSE(Track::exists(session, 0)); } @@ -36,16 +37,16 @@ TEST_F(DatabaseFixture, SingleTrack) { auto transaction {session.createSharedTransaction()}; - EXPECT_EQ(Track::getAll(session).size(), 1); + EXPECT_EQ(Track::find(session, Track::FindParameters {}).results.size(), 1); EXPECT_EQ(Track::getCount(session), 1); EXPECT_TRUE(Track::exists(session, track.getId())); - auto myTrack {Track::getById(session, track.getId())}; + auto myTrack {Track::find(session, track.getId())}; ASSERT_TRUE(myTrack); EXPECT_EQ(myTrack->getId(), track.getId()); } } -TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) +TEST_F(DatabaseFixture, Track_findByKeywords) { ScopedTrack track1 {session, ""}; ScopedTrack track2 {session, ""}; @@ -67,32 +68,31 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) { auto transaction {session.createSharedTransaction()}; - bool more; { - const auto tracks {Track::getByFilter(session, {}, {"Track"}, std::nullopt, more)}; - EXPECT_EQ(tracks.size(), 6); + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"Track"}))}; + EXPECT_EQ(tracks.results.size(), 6); } { - const auto tracks {Track::getByFilter(session, {}, {"MyTrack"}, std::nullopt, more)}; - EXPECT_EQ(tracks.size(), 5); - EXPECT_TRUE(std::none_of(std::cbegin(tracks), std::cend(tracks), [&](const Track::pointer& track) { return track->getId() == track6.getId(); })); + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack"}))}; + EXPECT_EQ(tracks.results.size(), 5); + EXPECT_TRUE(std::none_of(std::cbegin(tracks.results), std::cend(tracks.results), [&](const TrackId trackId) { return trackId == track6.getId(); })); } { - const auto tracks {Track::getByFilter(session, {}, {"MyTrack%"}, std::nullopt, more)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track2.getId()); - EXPECT_EQ(tracks[1]->getId(), track3.getId()); + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"MyTrack%"}))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track2.getId()); + EXPECT_EQ(tracks.results[1], track3.getId()); } { - const auto tracks {Track::getByFilter(session, {}, {"%MyTrack"}, std::nullopt, more)}; - ASSERT_EQ(tracks.size(), 2); - EXPECT_EQ(tracks[0]->getId(), track4.getId()); - EXPECT_EQ(tracks[1]->getId(), track5.getId()); + const auto tracks {Track::find(session, Track::FindParameters {}.setKeywords({"%MyTrack"}))}; + ASSERT_EQ(tracks.results.size(), 2); + EXPECT_EQ(tracks.results[0], track4.getId()); + EXPECT_EQ(tracks.results[1], track5.getId()); } } } -TEST_F(DatabaseFixture, SingleTrackDate) +TEST_F(DatabaseFixture, Track_date) { ScopedTrack track {session, "MyTrack"}; @@ -115,36 +115,33 @@ TEST_F(DatabaseFixture, SingleTrackDate) } } -TEST_F(DatabaseFixture, SingleStarredTrack) +TEST_F(DatabaseFixture, Track_writtenAfter) { ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; + + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20}}; { auto transaction {session.createUniqueTransaction()}; - - EXPECT_FALSE(user->isStarred(track.get())); + track.get().modify()->setLastWriteTime(dateTime); } { - auto transaction {session.createUniqueTransaction()}; - - user.get().modify()->star(track.get()); + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {})}; + EXPECT_EQ(tracks.results.size(), 1); } { - auto transaction {session.createUniqueTransaction()}; + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1)))}; + EXPECT_EQ(tracks.results.size(), 1); + } - EXPECT_TRUE(user->isStarred(track.get())); - - bool hasMore {}; - auto tracks {Track::getStarred(session, user.get(), {}, std::nullopt, hasMore)}; - ASSERT_EQ(tracks.size(), 1); - EXPECT_EQ(tracks.front()->getId(), track.getId()); - EXPECT_FALSE(hasMore); + { + auto transaction {session.createSharedTransaction()}; + const auto tracks {Track::find(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1)))}; + EXPECT_EQ(tracks.results.size(), 0); } } - - - diff --git a/src/libs/services/database/test/TrackBookmark.cpp b/src/libs/services/database/test/TrackBookmark.cpp new file mode 100644 index 00000000..d99b0bd5 --- /dev/null +++ b/src/libs/services/database/test/TrackBookmark.cpp @@ -0,0 +1,66 @@ +/* + * 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 "services/database/TrackBookmark.hpp" + +using ScopedTrackBookmark = ScopedEntity; + +using namespace Database; + +TEST_F(DatabaseFixture, TrackBookmark) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackBookmark::getCount(session), 0); + } + + ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()}; + + { + auto transaction {session.createUniqueTransaction()}; + + bookmark.get().modify()->setComment("MyComment"); + bookmark.get().modify()->setOffset(std::chrono::milliseconds {5}); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(TrackBookmark::getCount(session), 1); + + const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})}; + ASSERT_EQ(bookmarks.results.size(), 1); + EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); + } + { + auto transaction {session.createSharedTransaction()}; + + auto userBookmark {TrackBookmark::find(session, user.getId(), track.getId())}; + ASSERT_TRUE(userBookmark); + EXPECT_EQ(userBookmark, bookmark.get()); + + EXPECT_EQ(userBookmark->getOffset(), std::chrono::milliseconds {5}); + EXPECT_EQ(userBookmark->getComment(), "MyComment"); + } +} diff --git a/src/libs/services/database/test/TrackFeatures.cpp b/src/libs/services/database/test/TrackFeatures.cpp new file mode 100644 index 00000000..5d919a40 --- /dev/null +++ b/src/libs/services/database/test/TrackFeatures.cpp @@ -0,0 +1,48 @@ +/* + * 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 "services/database/TrackFeatures.hpp" + +using ScopedTrackFeatures = ScopedEntity; + +using namespace Database; + +TEST_F(DatabaseFixture, TrackFeatures) +{ + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackFeatures::getCount(session), 0); + } + + ScopedTrackFeatures trackFeatures {session, track.lockAndGet(), ""}; + + { + auto transaction {session.createUniqueTransaction()}; + EXPECT_EQ(TrackFeatures::getCount(session), 1); + + auto allTrackFeatures {TrackFeatures::find(session, Range {})}; + ASSERT_EQ(allTrackFeatures.results.size(), 1); + EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId()); + } +} diff --git a/src/libs/services/database/test/TrackList.cpp b/src/libs/services/database/test/TrackList.cpp new file mode 100644 index 00000000..9bdb96ec --- /dev/null +++ b/src/libs/services/database/test/TrackList.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2019 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 + +#include "Common.hpp" + +using namespace Database; + +TEST_F(DatabaseFixture, SingleTrackList) +{ + ScopedUser user {session, "MyUser"}; + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackList::getCount(session), 0); + } + + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + + { + auto transaction {session.createSharedTransaction()}; + EXPECT_EQ(TrackList::getCount(session), 1); + } +} + +TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + std::list tracks; + + for (std::size_t i {}; i < 10; ++i) + { + tracks.emplace_back(session, "MyTrack" + std::to_string(i)); + + auto transaction {session.createUniqueTransaction()}; + TrackListEntry::create(session, tracks.back().get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_EQ(trackList->getCount(), tracks.size()); + const auto trackIds {trackList->getTrackIds()}; + ASSERT_EQ(trackIds.size(), tracks.size()); + + // Same order + std::size_t i {}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), trackIds[i++]); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(reverseTracks.size(), tracks.size()); + + // Reverse order + ASSERT_TRUE(tracks.size() > 0); + std::size_t i {tracks.size() - 1}; + for (const ScopedTrack& track : tracks) + EXPECT_EQ(track.getId(), reverseTracks[i--]->getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedTrack track3 {session, "MyTrack3"}; + + { + Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; + auto transaction {session.createUniqueTransaction()}; + TrackListEntry::create(session, track1.get(), trackList.get(), now); + TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(-1)); + TrackListEntry::create(session, track3.get(), trackList.get(), now.addSecs(1)); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults; + const auto tracks {trackList.get()->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(tracks.size(), 3); + EXPECT_EQ(tracks.front()->getId(), track3.getId()); + EXPECT_EQ(tracks.back()->getId(), track2.getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MyTrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack1"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedArtist artist2 {session, "MyArtist2"}; + ScopedRelease release1 {session, "MyRelease1"}; + ScopedRelease release2 {session, "MyRelease2"}; + + const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; + + { + auto transaction {session.createUniqueTransaction()}; + + track1.get().modify()->setRelease(release1.get()); + track2.get().modify()->setRelease(release2.get()); + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + } + { + + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); + EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + TrackListEntry::create(session, track1.get(), trackList.get(), now); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front()->getId(), release1.getId()); + + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + EXPECT_EQ(tracks.size(), 1); + } + + { + auto transaction {session.createUniqueTransaction()}; + + TrackListEntry::create(session, track2.get(), trackList.get(), now.addSecs(1)); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0]->getId(), artist2.getId()); + EXPECT_EQ(artists[1]->getId(), artist1.getId()); + + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release2.getId()); + EXPECT_EQ(releases[1]->getId(), release1.getId()); + + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0]->getId(), track2.getId()); + EXPECT_EQ(tracks[1]->getId(), track1.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + TrackListEntry::create(session, track1.get(), trackList.get(), now.addSecs(2)); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists[0]->getId(), artist1.getId()); + EXPECT_EQ(artists[1]->getId(), artist2.getId()); + + const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(releases.size(), 2); + EXPECT_EQ(releases[0]->getId(), release1.getId()); + EXPECT_EQ(releases[1]->getId(), release2.getId()); + + const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)}; + ASSERT_EQ(tracks.size(), 2); + EXPECT_EQ(tracks[0]->getId(), track1.getId()); + EXPECT_EQ(tracks[1]->getId(), track2.getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackList_getArtists) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack2"}; + ScopedRelease release {session, "MyRelease"}; + ScopedArtist artist1 {session, "MyArtist1"}; + ScopedArtist artist2 {session, "MyArtist2"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(artists.empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); + TrackListEntry::create(session, track1.get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 1); + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + EXPECT_TRUE(artists.empty()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 1); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); + TrackListEntry::create(session, track2.get(), trackList.get()); + artist1.get().modify()->setSortName("ZZZ"); + artist2.get().modify()->setSortName("AAA"); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists.front()->getId(), artist1.getId()); + EXPECT_EQ(artists.back()->getId(), artist2.getId()); + } + + { + auto transaction {session.createSharedTransaction()}; + + bool moreResults {}; + const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(artists.size(), 2); + EXPECT_EQ(artists.front()->getId(), artist2.getId()); + EXPECT_EQ(artists.back()->getId(), artist1.getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackList_getReleases) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track {session, "MyTrack"}; + ScopedRelease release {session, "MyRelease"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(releases.empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + + track.get().modify()->setRelease(release.get()); + + TrackListEntry::create(session, track.get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_EQ(trackList->getCount(), 1); + bool moreResults {}; + const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(releases.size(), 1); + ASSERT_EQ(releases.front()->getId(), release->getId()); + } +} + +TEST_F(DatabaseFixture, SingleTrackList_getTracks) +{ + ScopedUser user {session, "MyUser"}; + ScopedTrackList trackList {session, "MytrackList", TrackList::Type::Playlist, false, user.lockAndGet()}; + ScopedTrack track1 {session, "MyTrack1"}; + ScopedTrack track2 {session, "MyTrack1"}; + + { + auto transaction {session.createSharedTransaction()}; + + EXPECT_EQ(trackList->getCount(), 0); + bool moreResults {}; + const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_TRUE(tracks.empty()); + } + + { + auto transaction {session.createUniqueTransaction()}; + TrackListEntry::create(session, track1.get(), trackList.get()); + TrackListEntry::create(session, track2.get(), trackList.get()); + TrackListEntry::create(session, track1.get(), trackList.get()); + } + + { + auto transaction {session.createSharedTransaction()}; + + ASSERT_EQ(trackList->getCount(), 3); + bool moreResults {}; + const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)}; + ASSERT_EQ(tracks.size(), 2); + ASSERT_EQ(tracks[0]->getId(), track1->getId()); + ASSERT_EQ(tracks[1]->getId(), track2->getId()); + } +} diff --git a/src/libs/services/feedback/CMakeLists.txt b/src/libs/services/feedback/CMakeLists.txt deleted file mode 100644 index 3049f60f..00000000 --- a/src/libs/services/feedback/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ - -add_library(lmsfeedback SHARED - impl/FeedbackService.cpp - ) - -target_include_directories(lmsfeedback INTERFACE - include - ) - -target_include_directories(lmsfeedback PRIVATE - include - ) - -target_link_libraries(lmsfeedback PRIVATE - lmsdatabase - ) - -install(TARGETS lmsfeedback DESTINATION lib) - diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp deleted file mode 100644 index d4c0a04f..00000000 --- a/src/libs/services/feedback/impl/FeedbackService.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 "FeedbackService.hpp" -#include "services/database/Db.hpp" -#include "services/database/Artist.hpp" -#include "services/database/Release.hpp" -#include "services/database/Session.hpp" -#include "services/database/Track.hpp" -#include "services/database/User.hpp" - -namespace -{ - using namespace Database; - - void - exec(Db& db, UserId userId, ArtistId artistId, std::function func) - { - Session& dbSession {db.getTLSSession()}; - auto transaction {dbSession.createSharedTransaction()}; - - Database::User::pointer user {Database::User::getById(dbSession, userId)}; - if (!user) - return; - - Database::Artist::pointer artist {Database::Artist::getById(dbSession, artistId)}; - if (!artist) - return; - - func(user, artist); - } - - void - exec(Db& db, UserId userId, ReleaseId releaseId, std::function func) - { - Session& dbSession {db.getTLSSession()}; - auto transaction {dbSession.createSharedTransaction()}; - - Database::User::pointer user {Database::User::getById(dbSession, userId)}; - if (!user) - return; - - Database::Release::pointer release {Database::Release::getById(dbSession, releaseId)}; - if (!release) - return; - - func(user, release); - } - - void - exec(Db& db, UserId userId, TrackId trackId, std::function func) - { - Session& dbSession {db.getTLSSession()}; - auto transaction {dbSession.createSharedTransaction()}; - - Database::User::pointer user {Database::User::getById(dbSession, userId)}; - if (!user) - return; - - Database::Track::pointer track {Database::Track::getById(dbSession, trackId)}; - if (!track) - return; - - func(user, track); - } -} - -namespace Feedback -{ - std::unique_ptr - createFeedbackService(Database::Db& db) - { - return std::make_unique(db); - } - - FeedbackService::FeedbackService(Database::Db& db) - : _db {db} - { - } - - void - FeedbackService::star(Database::UserId userId, Database::ArtistId artistId) - { - starObject(userId, artistId); - } - - void - FeedbackService::unstar(Database::UserId userId, Database::ArtistId artistId) - { - unstarObject(userId, artistId); - } - - bool - FeedbackService::isStarred(Database::UserId userId, Database::ArtistId artistId) - { - return isObjectStarred(userId, artistId); - } - - void - FeedbackService::star(Database::UserId userId, Database::ReleaseId releaseId) - { - starObject(userId, releaseId); - } - - void - FeedbackService::unstar(Database::UserId userId, Database::ReleaseId releaseId) - { - unstarObject(userId, releaseId); - } - - bool - FeedbackService::isStarred(Database::UserId userId, Database::ReleaseId releaseId) - { - return isObjectStarred(userId, releaseId); - } - - void - FeedbackService::star(Database::UserId userId, Database::TrackId trackId) - { - starObject(userId, trackId); - } - - void - FeedbackService::unstar(Database::UserId userId, Database::TrackId trackId) - { - unstarObject(userId, trackId); - } - - bool - FeedbackService::isStarred(Database::UserId userId, Database::TrackId trackId) - { - return isObjectStarred(userId, trackId); - } - - template - void - FeedbackService::starObject(Database::UserId userId, DatabaseId objectId) - { - exec(_db, userId, objectId, [](Database::User::pointer user, auto object) - { - user.modify()->star(object); - }); - } - - template - void - FeedbackService::unstarObject(Database::UserId userId, DatabaseId objectId) - { - exec(_db, userId, objectId, [](Database::User::pointer user, auto object) - { - user.modify()->unstar(object); - }); - } - - template - bool - FeedbackService::isObjectStarred(Database::UserId userId, DatabaseId objectId) - { - bool res {}; - exec(_db, userId, objectId, [&res](Database::User::pointer user, auto object) - { - res = user->isStarred(object); - }); - return res; - } -} // ns Feedback - diff --git a/src/libs/services/feedback/impl/FeedbackService.hpp b/src/libs/services/feedback/impl/FeedbackService.hpp deleted file mode 100644 index c2b37b13..00000000 --- a/src/libs/services/feedback/impl/FeedbackService.hpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 . - */ - -#pragma once - -#include "services/feedback/IFeedbackService.hpp" - -namespace Database -{ - class Db; -} - -namespace Feedback -{ - class FeedbackService : public IFeedbackService - { - public: - FeedbackService(Database::Db& db); - - private: - void star(Database::UserId userId, Database::ArtistId artistId) override; - void unstar(Database::UserId userId, Database::ArtistId artistId) override; - bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; - - void star(Database::UserId userId, Database::ReleaseId releaseId) override; - void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; - bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override; - - void star(Database::UserId userId, Database::TrackId trackId) override; - void unstar(Database::UserId userId, Database::TrackId trackId) override; - bool isStarred(Database::UserId userId, Database::TrackId trackId) override; - - template - void starObject(Database::UserId userId, DatabaseId objectId); - template - void unstarObject(Database::UserId userId, DatabaseId objectId); - template - bool isObjectStarred(Database::UserId userId, DatabaseId objectId); - - Database::Db& _db; - }; -} // ns Feedback - diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp deleted file mode 100644 index 67b89d62..00000000 --- a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 . - */ - -#pragma once - -#include "services/database/Types.hpp" - -namespace Database -{ - class Db; -} - -namespace Feedback -{ - class IFeedbackService - { - public: - virtual ~IFeedbackService() = default; - - virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; - virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; - - virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; - virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; - virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; - - virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; - virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; - virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; - }; - - std::unique_ptr createFeedbackService(Database::Db& db); -} // ns Feedback - diff --git a/src/libs/services/recommendation/impl/IEngine.hpp b/src/libs/services/recommendation/impl/IEngine.hpp index b4b9da65..789ef7cf 100644 --- a/src/libs/services/recommendation/impl/IEngine.hpp +++ b/src/libs/services/recommendation/impl/IEngine.hpp @@ -19,12 +19,11 @@ #pragma once -#include #include -#include #include "services/database/Types.hpp" -#include "utils/EnumSet.hpp" +#include "services/database/TrackListId.hpp" #include "services/recommendation/Types.hpp" +#include "utils/EnumSet.hpp" namespace Database { @@ -41,8 +40,8 @@ namespace Recommendation virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void requestCancelLoad() = 0; - virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; - virtual TrackContainer getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0; virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const = 0; }; diff --git a/src/libs/services/recommendation/impl/RecommendationService.cpp b/src/libs/services/recommendation/impl/RecommendationService.cpp index 59a805ec..76e4e693 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.cpp +++ b/src/libs/services/recommendation/impl/RecommendationService.cpp @@ -59,7 +59,7 @@ namespace Recommendation } TrackContainer - RecommendationService::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const + RecommendationService::findSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const { TrackContainer res; @@ -70,7 +70,7 @@ namespace Recommendation if (itEngine == std::cend(_engines)) continue; - res = itEngine->second->getSimilarTracksFromTrackList(trackListId, maxCount); + res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount); if (!res.empty()) break; } @@ -79,7 +79,7 @@ namespace Recommendation } TrackContainer - RecommendationService::getSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const + RecommendationService::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const { TrackContainer res; @@ -91,7 +91,7 @@ namespace Recommendation continue; const IEngine& engine {*itEngine->second}; - res = engine.getSimilarTracks(trackIds, maxCount); + res = engine.findSimilarTracks(trackIds, maxCount); if (!res.empty()) { LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'"; @@ -140,6 +140,8 @@ namespace Recommendation if (itEngine == std::cend(_engines)) continue; + LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "'"; + const IEngine& engine {*itEngine->second}; res = engine.getSimilarArtists(artistId, linkTypes, maxCount); if (!res.empty()) diff --git a/src/libs/services/recommendation/impl/RecommendationService.hpp b/src/libs/services/recommendation/impl/RecommendationService.hpp index 22e07217..25e6b5d4 100644 --- a/src/libs/services/recommendation/impl/RecommendationService.hpp +++ b/src/libs/services/recommendation/impl/RecommendationService.hpp @@ -56,8 +56,8 @@ namespace Recommendation void load(bool forceReload, const ProgressCallback& progressCallback) override; void cancelLoad() override; - TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; - TrackContainer getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp index f2f3d458..9a714ff7 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -29,40 +29,35 @@ namespace Recommendation { -std::unique_ptr createClustersEngine(Database::Db& db) +using namespace Database; + +std::unique_ptr createClustersEngine(Db& db) { return std::make_unique(db); } TrackContainer -ClusterEngine::getSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const +ClusterEngine::findSimilarTracks(const std::vector& trackIds, std::size_t maxCount) const { - Database::Session& dbSession {_db.getTLSSession()}; + Session& dbSession {_db.getTLSSession()}; - TrackContainer res; + auto transaction {dbSession.createSharedTransaction()}; - { - auto transaction {dbSession.createSharedTransaction()}; - - const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)}; - res.reserve(tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); }); - } - - return res; + const auto similarTrackIds {Track::findSimilarTracks(dbSession, trackIds, Range {0, maxCount})}; + return std::move(similarTrackIds.results); } TrackContainer -ClusterEngine::getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const +ClusterEngine::findSimilarTracksFromTrackList(TrackListId tracklistId, std::size_t maxCount) const { - Database::Session& dbSession {_db.getTLSSession()}; + Session& dbSession {_db.getTLSSession()}; TrackContainer res; { auto transaction {dbSession.createSharedTransaction()}; - const Database::TrackList::pointer trackList {Database::TrackList::getById(dbSession, tracklistId)}; + const TrackList::pointer trackList {TrackList::find(dbSession, tracklistId)}; if (!trackList) return res; @@ -75,15 +70,15 @@ ClusterEngine::getSimilarTracksFromTrackList(Database::TrackListId tracklistId, } ReleaseContainer -ClusterEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const +ClusterEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const { - Database::Session& dbSession {_db.getTLSSession()}; + Session& dbSession {_db.getTLSSession()}; ReleaseContainer res; { auto transaction {dbSession.createSharedTransaction()}; - auto release {Database::Release::getById(dbSession, releaseId)}; + auto release {Release::find(dbSession, releaseId)}; if (!release) return res; @@ -96,24 +91,18 @@ ClusterEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t max } ArtistContainer -ClusterEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet artistLinkTypes, std::size_t maxCount) const +ClusterEngine::getSimilarArtists(ArtistId artistId, EnumSet artistLinkTypes, std::size_t maxCount) const { - Database::Session& dbSession {_db.getTLSSession()}; + Session& dbSession {_db.getTLSSession()}; - ResultContainer res; - { - auto transaction {dbSession.createSharedTransaction()}; + auto transaction {dbSession.createSharedTransaction()}; - auto artist {Database::Artist::getById(dbSession, artistId)}; - if (!artist) - return res; + auto artist {Artist::find(dbSession, artistId)}; + if (!artist) + return {}; - const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})}; - res.reserve(artists.size()); - std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); }); - } - - return res; + const auto similarArtistIds {artist->findSimilarArtists(artistLinkTypes, Range {0, maxCount})}; + return std::move(similarArtistIds.results); } } // namespace Recommendation diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp index faff863c..09c14072 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.hpp @@ -38,8 +38,8 @@ namespace Recommendation void load(bool, const ProgressCallback&) override {} void requestCancelLoad() override {} - TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; - TrackContainer getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp index bb030bd6..daee3d4b 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngine.cpp @@ -36,7 +36,9 @@ namespace Recommendation { -std::unique_ptr createFeaturesEngine(Database::Db& db) +using namespace Database; + +std::unique_ptr createFeaturesEngine(Db& db) { return std::make_unique(db); } @@ -49,44 +51,13 @@ FeaturesEngine::getDefaultTrainFeatureSettings() { "lowlevel.spectral_energyband_high.mean", {1}}, { "lowlevel.spectral_rolloff.median", {1}}, { "lowlevel.spectral_contrast_valleys.var", {1}}, - { "lowlevel.erbbands.mean", {1}}, - { "lowlevel.gfcc.mean", {1}}, + { "lowlevel.erbbands.mean", {1}}, + { "lowlevel.gfcc.mean", {1}}, }; return defaultTrainFeatureSettings; } -static -std::optional -getTrackFeatureValues(FeaturesEngine::FeaturesFetchFunc func, Database::TrackId trackId, const std::unordered_set& featureNames) -{ - return func(trackId, featureNames); -} - -static -std::optional -getTrackFeatureValuesFromDb(Database::Session& session, Database::TrackId trackId, const std::unordered_set& featureNames) -{ - auto func = [&](Database::TrackId trackId, const std::unordered_set& featureNames) - { - std::optional res; - - auto transaction {session.createSharedTransaction()}; - - Database::Track::pointer track {Database::Track::getById(session, trackId)}; - if (!track) - return res; - - res = track->getTrackFeatures()->getFeatureValuesMap(featureNames); - if (res->empty()) - res.reset(); - - return res; - }; - - return getTrackFeatureValues(func, trackId, featureNames); -} - static std::optional convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions) @@ -142,45 +113,46 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions; - Database::Session& session {_db.getTLSSession()}; + Session& session {_db.getTLSSession()}; - std::vector trackIds; + RangeResults trackFeaturesIds; { auto transaction {session.createSharedTransaction()}; - LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features..."; - trackIds = Database::Track::getAllIdsWithFeatures(session); - LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features..."; + trackFeaturesIds = TrackFeatures::find(session, Range {}); + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)"; } std::vector samples; - std::vector samplesTrackIds; + std::vector samplesTrackIds; - samples.reserve(trackIds.size()); - samplesTrackIds.reserve(trackIds.size()); + samples.reserve(trackFeaturesIds.results.size()); + samplesTrackIds.reserve(trackFeaturesIds.results.size()); LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features..."; - for (Database::TrackId trackId : trackIds) + // TODO handle errors using exceptions + for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results) { if (_loadCancelled) return; - std::optional featureValuesMap; + auto transaction {session.createSharedTransaction()}; - if (_featuresFetchFunc) - featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames); - else - featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames); - - if (!featureValuesMap) + TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)}; + if (!trackFeatures) continue; - std::optional inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)}; + FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)}; + if (featureValuesMap.empty()) + continue; + + std::optional inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)}; if (!inputVector) continue; samples.emplace_back(std::move(*inputVector)); - samplesTrackIds.emplace_back(trackId); + samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId()); } LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE"; @@ -249,41 +221,41 @@ FeaturesEngine::loadFromCache(FeaturesEngineCache cache) } TrackContainer -FeaturesEngine::getSimilarTracksFromTrackList(Database::TrackListId trackListId, std::size_t maxCount) const +FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const { const TrackContainer trackIds {[&] { TrackContainer res; - Database::Session& session {_db.getTLSSession()}; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; + const TrackList::pointer trackList {TrackList::find(session, trackListId)}; if (trackList) res = trackList->getTrackIds(); return res; }()}; - return getSimilarTracks(trackIds, maxCount); + return findSimilarTracks(trackIds, maxCount); } TrackContainer -FeaturesEngine::getSimilarTracks(const std::vector& tracksIds, std::size_t maxCount) const +FeaturesEngine::findSimilarTracks(const std::vector& tracksIds, std::size_t maxCount) const { auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)}; - Database::Session& session {_db.getTLSSession()}; + Session& session {_db.getTLSSession()}; { // Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time) auto transaction {session.createSharedTransaction()}; similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds), - [&](Database::TrackId trackId) + [&](TrackId trackId) { - return !Database::Track::exists(session, trackId); + return !Track::exists(session, trackId); }), std::end(similarTrackIds)); } @@ -291,11 +263,11 @@ FeaturesEngine::getSimilarTracks(const std::vector& tracksIds } ReleaseContainer -FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const +FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const { auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)}; - Database::Session& session {_db.getTLSSession()}; + Session& session {_db.getTLSSession()}; if (!similarReleaseIds.empty()) { @@ -303,9 +275,9 @@ FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t ma auto transaction {session.createSharedTransaction()}; similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds), - [&](Database::ReleaseId releaseId) + [&](ReleaseId releaseId) { - return !Database::Release::exists(session, releaseId); + return !Release::exists(session, releaseId); }), std::end(similarReleaseIds)); } @@ -313,9 +285,9 @@ FeaturesEngine::getSimilarReleases(Database::ReleaseId releaseId, std::size_t ma } ArtistContainer -FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const +FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const { - auto getSimilarArtistIdsForLinkType {[&] (Database::TrackArtistLinkType linkType) + auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType) { ArtistContainer similarArtistIds; @@ -328,9 +300,9 @@ FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSetsecond, _artistPositions, maxCount); }}; - std::unordered_set similarArtistIds; + std::unordered_set similarArtistIds; - for (Database::TrackArtistLinkType linkType : linkTypes) + for (TrackArtistLinkType linkType : linkTypes) { const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)}; similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType)); @@ -338,15 +310,15 @@ FeaturesEngine::getSimilarArtists(Database::ArtistId artistId, EnumSet>>(Database::TrackId, const std::unordered_set& /*features*/)>; - // Default is to retrieve the features from the database (may be slow). - // Use this only if you want to train different searchers with some cached data - static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; } - static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); private: void load(bool forceReload, const ProgressCallback& progressCallback) override; void requestCancelLoad() override; - TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; - TrackContainer getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; + TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const override; + TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const override; ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const override; @@ -121,8 +116,6 @@ class FeaturesEngine : public IEngine TrackPositions _trackPositions; TrackMatrix _trackMatrix; - - static inline FeaturesFetchFunc _featuresFetchFunc; }; template diff --git a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp index 348c66af..a27712db 100644 --- a/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp +++ b/src/libs/services/recommendation/impl/features/FeaturesEngineCache.hpp @@ -21,9 +21,8 @@ #include #include -#include -#include "services/database/Types.hpp" +#include "services/database/TrackId.hpp" #include "som/Network.hpp" namespace Recommendation { diff --git a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp index 900c95a4..45704ac9 100644 --- a/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/IRecommendationService.hpp @@ -20,8 +20,9 @@ #pragma once #include -#include #include "utils/EnumSet.hpp" +#include "services/database/TrackListId.hpp" +#include "services/database/Types.hpp" #include "services/recommendation/Types.hpp" namespace Database @@ -39,8 +40,8 @@ namespace Recommendation virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void cancelLoad() = 0; // wait for cancel done - virtual TrackContainer getSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; - virtual TrackContainer getSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracksFromTrackList(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; + virtual TrackContainer findSimilarTracks(const std::vector& tracksId, std::size_t maxCount) const = 0; virtual ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const = 0; virtual ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet linkTypes, std::size_t maxCount) const = 0; }; diff --git a/src/libs/services/recommendation/include/services/recommendation/Types.hpp b/src/libs/services/recommendation/include/services/recommendation/Types.hpp index 0b258c3f..bfdaa67f 100644 --- a/src/libs/services/recommendation/include/services/recommendation/Types.hpp +++ b/src/libs/services/recommendation/include/services/recommendation/Types.hpp @@ -1,7 +1,9 @@ #pragma once #include -#include "services/database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" namespace Recommendation { diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index d96663be..d00c97ff 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -138,7 +138,7 @@ getOrCreateArtists(Session& session, const std::vector& artist // First try to get by MBID if (artistInfo.musicBrainzArtistID) { - artist = Artist::getByMBID(session, *artistInfo.musicBrainzArtistID); + artist = Artist::find(session, *artistInfo.musicBrainzArtistID); if (!artist) artist = createArtist(session, artistInfo); else @@ -151,7 +151,7 @@ getOrCreateArtists(Session& session, const std::vector& artist // Fall back on artist name (collisions may occur) if (!artistInfo.name.empty()) { - for (const Artist::pointer& sameNamedArtist : Artist::getByName(session, artistInfo.name)) + for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) { // Do not fallback on artist that is correctly tagged if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) @@ -183,7 +183,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) // First try to get by MBID if (album.musicBrainzAlbumID) { - release = Release::getByMBID(session, *album.musicBrainzAlbumID); + release = Release::find(session, *album.musicBrainzAlbumID); if (!release) { release = Release::create(session, album.name, album.musicBrainzAlbumID); @@ -200,7 +200,7 @@ getOrCreateRelease(Session& session, const MetaData::Album& album) // Fall back on release name (collisions may occur) if (!album.name.empty()) { - for (const Release::pointer& sameNamedRelease : Release::getByName(session, album.name)) + for (const Release::pointer& sameNamedRelease : Release::find(session, album.name)) { // do not fallback on properly tagged releases if (!sameNamedRelease->getMBID()) @@ -227,7 +227,7 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) for (auto clusterNames : clustersNames) { - auto clusterType = ClusterType::getByName(session, clusterNames.first); + auto clusterType = ClusterType::find(session, clusterNames.first); if (!clusterType) continue; @@ -249,12 +249,12 @@ getOrCreateClusters(Session& session, const MetaData::Clusters& clustersNames) namespace Scanner { std::unique_ptr -createScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService) +createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) { return std::make_unique(db, recommendationService); } -ScannerService::ScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService) +ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService) : _recommendationService {recommendationService} , _dbSession {db} { @@ -543,7 +543,7 @@ ScannerService::scan(bool forceScan) } bool -ScannerService::fetchTrackFeatures(Database::TrackId trackId, const UUID& recordingMBID) +ScannerService::fetchTrackFeatures(TrackId trackId, const UUID& recordingMBID) { std::map features; @@ -558,11 +558,11 @@ ScannerService::fetchTrackFeatures(Database::TrackId trackId, const UUID& record { auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - Database::Track::pointer track {Database::Track::getById(_dbSession, trackId)}; + Track::pointer track {Track::find(_dbSession, trackId)}; if (!track) return false; - Database::TrackFeatures::create(_dbSession, track, data); + TrackFeatures::create(_dbSession, track, data); } return true; @@ -580,7 +580,7 @@ ScannerService::fetchTrackFeatures(ScanStats& stats) struct TrackInfo { - Database::TrackId id; + TrackId id; UUID recordingMBID; }; @@ -590,9 +590,12 @@ ScannerService::fetchTrackFeatures(ScanStats& stats) auto transaction {_dbSession.createSharedTransaction()}; - auto tracks {Database::Track::getAllWithRecordingMBIDAndMissingFeatures(_dbSession)}; - for (const auto& track : tracks) + auto trackIds {Track::findWithRecordingMBIDAndMissingFeatures(_dbSession, Range {})}; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(_dbSession, trackId)}; res.emplace_back(TrackInfo {track->getId(), *track->getRecordingMBID()}); + } return res; }()}; @@ -692,7 +695,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, // Skip file if last write is the same auto transaction {_dbSession.createSharedTransaction()}; - const Track::pointer track {Track::getByPath(_dbSession, file)}; + const Track::pointer track {Track::findByPath(_dbSession, file)}; if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() && track->getScanVersion() == _scanVersion) @@ -713,7 +716,7 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - Track::pointer track {Track::getByPath(_dbSession, file) }; + Track::pointer track {Track::findByPath(_dbSession, file) }; // We estimate this is an audio file if: // - we found a least one audio stream @@ -778,30 +781,30 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, track.modify()->clearArtistLinks(); // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files for (const Artist::pointer& artist : getOrCreateArtists(_dbSession, trackInfo->artists, false)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, artist, Database::TrackArtistLinkType::Artist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, artist, TrackArtistLinkType::Artist)); for (const Artist::pointer& releaseArtist : getOrCreateArtists(_dbSession, trackInfo->albumArtists, false)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, releaseArtist, Database::TrackArtistLinkType::ReleaseArtist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process for (const Artist::pointer& conductor : getOrCreateArtists(_dbSession, trackInfo->conductorArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, conductor, Database::TrackArtistLinkType::Conductor)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, conductor, TrackArtistLinkType::Conductor)); for (const Artist::pointer& composer : getOrCreateArtists(_dbSession, trackInfo->composerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, composer, Database::TrackArtistLinkType::Composer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, composer, TrackArtistLinkType::Composer)); for (const Artist::pointer& lyricist : getOrCreateArtists(_dbSession, trackInfo->lyricistArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, lyricist, Database::TrackArtistLinkType::Lyricist)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, mixer, Database::TrackArtistLinkType::Mixer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer)); for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, producer, Database::TrackArtistLinkType::Producer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer)); for (const Artist::pointer& remixer : getOrCreateArtists(_dbSession, trackInfo->remixerArtists, true)) - track.modify()->addArtistLink(Database::TrackArtistLink::create(_dbSession, track, remixer, Database::TrackArtistLinkType::Remixer)); + track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, remixer, TrackArtistLinkType::Remixer)); track.modify()->setScanVersion(_scanVersion); if (trackInfo->album) @@ -827,7 +830,8 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan, track.modify()->setRecordingMBID(trackInfo->recordingMBID); track.modify()->setTrackMBID(trackInfo->trackMBID); - track.modify()->setFeatures({}); // TODO: only if MBID changed? + if (auto trackFeatures {TrackFeatures::find(_dbSession, track->getId())}) + trackFeatures.remove(); // TODO: only if MBID changed? track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyrightURL(trackInfo->copyrightURL); @@ -921,26 +925,25 @@ ScannerService::removeMissingTracks(ScanStats& stats) stepStats.totalElems = trackCount; notifyInProgress(stepStats); - std::vector> trackPaths; + RangeResults trackPaths; std::vector tracksToRemove; for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) { - trackPaths.clear(); tracksToRemove.clear(); { auto transaction {_dbSession.createSharedTransaction()}; - trackPaths = Track::getAllPaths(_dbSession, i, batchSize); + trackPaths = Track::findPaths(_dbSession, Range {i, batchSize}); } - for (const auto& [trackId, trackPath] : trackPaths) + for (const Track::PathResult& trackPath : trackPaths.results) { if (_abortScan) return; - if (!checkFile(trackPath, _mediaDirectory, _fileExtensions)) - tracksToRemove.push_back(trackId); + if (!checkFile(trackPath.path, _mediaDirectory, _fileExtensions)) + tracksToRemove.push_back(trackPath.trackId); stepStats.processedElems++; } @@ -951,7 +954,7 @@ ScannerService::removeMissingTracks(ScanStats& stats) for (const TrackId trackId : tracksToRemove) { - Track::pointer track {Track::getById(_dbSession, trackId)}; + Track::pointer track {Track::find(_dbSession, trackId)}; if (track) { track.remove(); @@ -977,9 +980,10 @@ ScannerService::removeOrphanEntries() auto transaction {_dbSession.createUniqueTransaction()}; // Now process orphan Cluster (no track) - auto clusters {Cluster::getAllOrphans(_dbSession)}; - for (auto& cluster : clusters) + auto clusterIds {Cluster::findOrphans(_dbSession, Range {})}; + for (ClusterId clusterId : clusterIds.results) { + Cluster::pointer cluster {Cluster::find(_dbSession, clusterId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; cluster.remove(); } @@ -989,9 +993,10 @@ ScannerService::removeOrphanEntries() { auto transaction {_dbSession.createUniqueTransaction()}; - auto artists {Artist::getAllOrphans(_dbSession)}; - for (auto& artist : artists) + auto artistIds {Artist::findAllOrphans(_dbSession, Range {})}; + for (const ArtistId artistId : artistIds.results) { + Artist::pointer artist {Artist::find(_dbSession, artistId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; artist.remove(); } @@ -1001,9 +1006,10 @@ ScannerService::removeOrphanEntries() { auto transaction {_dbSession.createUniqueTransaction()}; - auto releases {Release::getAllOrphans(_dbSession)}; - for (auto& release : releases) + auto releases {Release::findOrphans(_dbSession, Range {})}; + for (const ReleaseId releaseId : releases.results) { + Release::pointer release {Release::find(_dbSession, releaseId)}; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; release.remove(); } @@ -1019,9 +1025,10 @@ ScannerService::checkDuplicatedAudioFiles(ScanStats& stats) auto transaction {_dbSession.createSharedTransaction()}; - const std::vector tracks = Database::Track::getMBIDDuplicates(_dbSession); - for (const Track::pointer& track : tracks) + const RangeResults tracks = Track::findMBIDDuplicates(_dbSession, Range {}); + for (const TrackId trackId : tracks.results) { + const Track::pointer track {Track::find(_dbSession, trackId)}; if (auto trackMBID {track->getTrackMBID()}) { LMS_LOG(DBUPDATER, INFO) << "Found duplicated Track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName(); diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 530a946e..1b5e3de4 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -24,7 +24,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/TrackId.hpp" namespace Scanner { diff --git a/src/libs/services/scrobbling/impl/IScrobbler.hpp b/src/libs/services/scrobbling/impl/IScrobbler.hpp index cc0a98ca..3a41242d 100644 --- a/src/libs/services/scrobbling/impl/IScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/IScrobbler.hpp @@ -23,8 +23,9 @@ #include #include -#include - +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackListId.hpp" #include "services/scrobbling/Listen.hpp" namespace Database @@ -36,18 +37,26 @@ namespace Database namespace Scrobbling { - class IScrobbler { public: virtual ~IScrobbler() = default; + // Listens virtual void listenStarted(const Listen& listen) = 0; virtual void listenFinished(const Listen& listen, std::optional duration) = 0; - virtual void addTimedListen(const TimedListen& listen) = 0; - virtual Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) = 0; + // Feedback + virtual void onStarred(Database::UserId, Database::ArtistId) {}; + virtual void onUnstarred(Database::UserId, Database::ArtistId) {}; + virtual void onStarred(Database::UserId, Database::ReleaseId) {}; + virtual void onUnstarred(Database::UserId, Database::ReleaseId) {}; + virtual void onStarred(Database::UserId, Database::TrackId) {}; + virtual void onUnstarred(Database::UserId, Database::TrackId) {}; + +// virtual void star(Database::TrackId trackId) = 0; +// virtual void unstar(Database::TrackId trackId) = 0; }; std::unique_ptr createScrobbler(std::string_view backendName); diff --git a/src/libs/services/scrobbling/impl/ScrobblerBase.cpp b/src/libs/services/scrobbling/impl/ScrobblerBase.cpp new file mode 100644 index 00000000..a21d312b --- /dev/null +++ b/src/libs/services/scrobbling/impl/ScrobblerBase.cpp @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include "services/scrobbling/Listen.hpp" + +namespace Scrobbling +{ + ScrobblerBase::ScrobblerBase(Database::Db& db) + : _db {db} + { + } + + Database::TrackListId + ScrobberBase::getListensTrackList(Database::UserId userId, std::string_view trackListName) + { + std::string_view listensTrackListName {getListensTrackListName()}; + Database::Session& session {_db.getTLSSession()}; + + { + auto transaction {session.createSharedTransaction()}; + + if (auto trackList {Database::TrackList::get(session, listensTrackListName, Database::TrackList::Type::Internal, userId)}) + return trackList->getId(); + } + + { + auto transaction {session.createUniqueTransaction()}; + if (auto trackList {Database::TrackList::get(session, listensTrackListName, Database::TrackList::Type::Internal, userId)}) + return trackList->getId(); + + const Database::User::pointer user {Database::User::getById(session, userId)}; + if (!user) + return {}; + + Database::TrackList::pointer trackList {Database::TrackList::create(session, listensTrackListName, Database::TrackList::Type::Internal, false, user)}; + return trackList->getId(); + } + } + + bool + ScrobberBase::saveTimedListen(const TimedListen& listen, std::string_view trackListName) + { + const Database::TrackListId trackListId {getListensTrackList(listen.userId)}; + if (!trackListId) + return false; + + { + + return true; + } + } +} // ns Scrobbling + diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index 455ccc65..607c1f1e 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -18,13 +18,17 @@ */ #include "ScrobblingService.hpp" +#include "ScrobblingService.impl.hpp" #include "services/database/Artist.hpp" #include "services/database/Db.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" #include "services/database/Track.hpp" -#include "services/database/TrackList.hpp" #include "services/database/User.hpp" #include "internal/InternalScrobbler.hpp" @@ -43,202 +47,248 @@ namespace Scrobbling ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db) : _db {db} { - _scrobblers.emplace(Database::Scrobbler::Internal, std::make_unique(_db)); - _scrobblers.emplace(Database::Scrobbler::ListenBrainz, std::make_unique(ioContext, _db)); + _scrobblers.emplace(Scrobbler::Internal, std::make_unique(_db)); + _scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique(ioContext, _db)); } void ScrobblingService::listenStarted(const Listen& listen) { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->listenStarted(listen); } void ScrobblingService::listenFinished(const Listen& listen, std::optional duration) { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->listenFinished(listen, duration); } void ScrobblingService::addTimedListen(const TimedListen& listen) { - if (std::optional scrobbler {getUserScrobbler(listen.userId)}) + if (std::optional scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->addTimedListen(listen); } - std::optional - ScrobblingService::getUserScrobbler(Database::UserId userId) + std::optional + ScrobblingService::getUserScrobbler(UserId userId) { - std::optional scrobbler; + std::optional scrobbler; Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - if (const User::pointer user {User::getById(session, userId)}) + if (const User::pointer user {User::find(session, userId)}) scrobbler = user->getScrobbler(); return scrobbler; } ScrobblingService::ArtistContainer - ScrobblingService::getRecentArtists(UserId userId, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) + ScrobblingService::getRecentArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, Range range) { ArtistContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - const ObjectPtr history {getListensTrackList(session, user)}; - if (history) - { - for (const Artist::pointer& artist : history->getArtistsReverse(clusterIds, linkType, range, moreResults)) - res.push_back(artist->getId()); - } - - return res; + return Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range); } ScrobblingService::ReleaseContainer - ScrobblingService::getRecentReleases(Database::UserId userId, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) + ScrobblingService::getRecentReleases(UserId userId, const std::vector& clusterIds, Range range) { ReleaseContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - const ObjectPtr history {getListensTrackList(session, user)}; - if (history) - { - for (const Release::pointer& release : history->getReleasesReverse(clusterIds, range, moreResults)) - res.push_back(release->getId()); - } - - return res; + return Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range); } ScrobblingService::TrackContainer - ScrobblingService::getRecentTracks(Database::UserId userId, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) + ScrobblingService::getRecentTracks(UserId userId, const std::vector& clusterIds, Range range) { TrackContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - const ObjectPtr history {getListensTrackList(session, user)}; - if (history) - { - for (const Track::pointer& track : history->getTracksReverse(clusterIds, range, moreResults)) - res.push_back(track->getId()); - } - - return res; + return Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range); } - // Top ScrobblingService::ArtistContainer - ScrobblingService::getTopArtists(UserId userId, - const std::vector& clusterIds, - std::optional linkType, - std::optional range, - bool& moreResults) + ScrobblingService::getTopArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, Range range) { ArtistContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - const ObjectPtr history {getListensTrackList(session, user)}; - if (history) - { - for (const Artist::pointer& artist : history->getTopArtists(clusterIds, linkType, range, moreResults)) - res.push_back(artist->getId()); - } - - return res; + return Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range); } ScrobblingService::ReleaseContainer - ScrobblingService::getTopReleases(Database::UserId userId, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) + ScrobblingService::getTopReleases(UserId userId, const std::vector& clusterIds, Range range) { ReleaseContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - const ObjectPtr history {getListensTrackList(session, user)}; - if (history) - { - for (const Release::pointer& release : history->getTopReleases(clusterIds, range, moreResults)) - res.push_back(release->getId()); - } - - return res; + return Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range); } ScrobblingService::TrackContainer - ScrobblingService::getTopTracks(Database::UserId userId, - const std::vector& clusterIds, - std::optional range, - bool& moreResults) + ScrobblingService::getTopTracks(UserId userId, const std::vector& clusterIds, Range range) { TrackContainer res; + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return res; + Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - const User::pointer user {User::getById(session, userId)}; - if (!user) - return res; - - if (const ObjectPtr history {getListensTrackList(session, user)}) - { - for (const Track::pointer& track : history->getTopTracks(clusterIds, range, moreResults)) - res.push_back(track->getId()); - } - - return res; + return Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range); } - Database::ObjectPtr - ScrobblingService::getListensTrackList(Session& session, Database::ObjectPtr user) + void + ScrobblingService::star(UserId userId, ArtistId artistId) { - return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user); + star(userId, artistId); } + void + ScrobblingService::unstar(UserId userId, ArtistId artistId) + { + unstar(userId, artistId); + } + + bool + ScrobblingService::isStarred(UserId userId, ArtistId artistId) + { + return isStarred(userId, artistId); + } + + ScrobblingService::ArtistContainer + ScrobblingService::getStarredArtists(UserId userId, const std::vector& clusterIds, + std::optional linkType, + ArtistSortMethod sortMethod, + Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Artist::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setLinkType(linkType); + params.setSortMethod(sortMethod); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Artist::find(session, params); + } + + void + ScrobblingService::star(UserId userId, ReleaseId releaseId) + { + star(userId, releaseId); + } + + void + ScrobblingService::unstar(UserId userId, ReleaseId releaseId) + { + unstar(userId, releaseId); + } + + bool + ScrobblingService::isStarred(UserId userId, ReleaseId releaseId) + { + return isStarred(userId, releaseId); + } + + ScrobblingService::ReleaseContainer + ScrobblingService::getStarredReleases(UserId userId, const std::vector& clusterIds, Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Release::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setSortMethod(ReleaseSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Release::find(session, params); + } + + void + ScrobblingService::star(UserId userId, TrackId trackId) + { + star(userId, trackId); + } + + void + ScrobblingService::unstar(UserId userId, TrackId trackId) + { + unstar(userId, trackId); + } + + bool + ScrobblingService::isStarred(UserId userId, TrackId trackId) + { + return isStarred(userId, trackId); + } + + ScrobblingService::TrackContainer + ScrobblingService::getStarredTracks(UserId userId, const std::vector& clusterIds, Range range) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return {}; + + Track::FindParameters params; + params.setStarringUser(userId, *scrobbler); + params.setClusters(clusterIds); + params.setSortMethod(TrackSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return Track::find(session, params); + } } // ns Scrobbling diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.hpp index 6b1b9959..63377ad6 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.hpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.hpp @@ -41,39 +41,57 @@ namespace Scrobbling ArtistContainer getRecentArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, - std::optional range, - bool& moreResults) override; + Database::Range range) override; ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; + Database::Range range) override; TrackContainer getRecentTracks(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; + Database::Range range) override; ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, - std::optional range, - bool& moreResults) override; + Database::Range range) override; ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; + Database::Range range) override; TrackContainer getTopTracks(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) override; + Database::Range range) override; - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); + void star(Database::UserId userId, Database::ArtistId artistId) override; + void unstar(Database::UserId userId, Database::ArtistId artistId) override; + bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; + ArtistContainer getStarredArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::ArtistSortMethod sortMethod, + Database::Range range) override; + + void star(Database::UserId userId, Database::ReleaseId releaseId) override; + void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; + bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override; + ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + + void star(Database::UserId userId, Database::TrackId trackId) override; + void unstar(Database::UserId userId, Database::TrackId trackId) override; + bool isStarred(Database::UserId userId, Database::TrackId trackId) override; + TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; std::optional getUserScrobbler(Database::UserId userId); + template + void star(Database::UserId userId, ObjIdType id); + template + void unstar(Database::UserId userId, ObjIdType id); + template + bool isStarred(Database::UserId userId, ObjIdType id); + Database::Db& _db; std::unordered_map> _scrobblers; }; diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp new file mode 100644 index 00000000..1b7ef03d --- /dev/null +++ b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp @@ -0,0 +1,93 @@ +/* + * 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 . + */ + +#pragma once + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/User.hpp" + +namespace Scrobbling +{ + using namespace Database; + + template + void + ScrobblingService::star(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return; + + { + Session& session {_db.getTLSSession()}; + auto transaction {session.createUniqueTransaction()}; + + typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; + if (!starredObj) + { + const typename ObjType::pointer obj {ObjType::find(session, objId)}; + if (!obj) + return; + + const User::pointer user {User::find(session, userId)}; + if (!user) + return; + + starredObj = StarredObjType::create(session, obj, user, *scrobbler); + } + starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); + } + _scrobblers[*scrobbler]->onStarred(userId, objId); + } + + template + void + ScrobblingService::unstar(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return; + + { + Session& session {_db.getTLSSession()}; + auto transaction {session.createUniqueTransaction()}; + + if (typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}) + starredObj.remove(); + } + _scrobblers[*scrobbler]->onUnstarred(userId, objId); + } + + template + bool + ScrobblingService::isStarred(UserId userId, ObjIdType objId) + { + auto scrobbler {getUserScrobbler(userId)}; + if (!scrobbler) + return false; + + Session& session {_db.getTLSSession()}; + auto transaction {session.createSharedTransaction()}; + + return StarredObjType::find(session, objId, userId, *scrobbler); + } + +} // ns Scrobbling + diff --git a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp index b0dc6964..3c2f17d4 100644 --- a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp +++ b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp @@ -20,22 +20,19 @@ #include "InternalScrobbler.hpp" #include "services/database/Db.hpp" +#include "services/database/Listen.hpp" #include "services/database/Session.hpp" -#include "services/database/Track.hpp" -#include "services/database/TrackList.hpp" #include "services/database/User.hpp" -#include "utils/Logger.hpp" +#include "services/database/Track.hpp" namespace Scrobbling { - static const std::string historyTracklistName {"__scrobbler_internal_history__"}; - InternalScrobbler::InternalScrobbler(Database::Db& db) - : _db {db} + : _db {db} {} void - InternalScrobbler::listenStarted(const Listen& /*listen*/) + InternalScrobbler::listenStarted(const Listen&) { // nothing to do } @@ -43,7 +40,7 @@ namespace Scrobbling void InternalScrobbler::listenFinished(const Listen& listen, std::optional duration) { - // record tracks that have been played for at least of few seconds... + // only record tracks that have been played for at least of few seconds... if (duration && *duration < std::chrono::seconds {5}) return; @@ -54,29 +51,20 @@ namespace Scrobbling InternalScrobbler::addTimedListen(const TimedListen& listen) { Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; - const Database::User::pointer user {Database::User::getById(session, listen.userId)}; + if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt)) + return; + + const Database::User::pointer user {Database::User::find(session, listen.userId)}; if (!user) return; - Database::TrackList::pointer tracklist {getListensTrackList(session, user)}; - if (!tracklist) - tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user); - - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; if (!track) return; - Database::TrackListEntry::create(session, track, getListensTrackList(session, user), listen.listenedAt); + Database::Listen::create(session, user, track, Database::Scrobbler::Internal, listen.listenedAt); } - - Database::TrackList::pointer - InternalScrobbler::getListensTrackList(Database::Session& session, Database::ObjectPtr user) - { - return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user); - } - } // Scrobbling diff --git a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp index 45c067da..1c4d7df4 100644 --- a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp @@ -34,14 +34,12 @@ namespace Scrobbling InternalScrobbler(Database::Db& db); private: + // IScrobbler void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; - void addTimedListen(const TimedListen& listen) override; - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; - - Database::Db& _db; + Database::Db& _db; }; } // Scrobbling diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp index 20b68fc0..db614b18 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp @@ -46,7 +46,7 @@ namespace { auto transaction {session.createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(session, trackId)}; + const Database::Track::pointer track {Database::Track::find(session, trackId)}; if (!track) return false; @@ -62,7 +62,7 @@ namespace { auto transaction {session.createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; if (!track) return std::nullopt; @@ -179,12 +179,6 @@ namespace Scrobbling::ListenBrainz enqueListen(listen, listen.listenedAt); } - Database::TrackList::pointer - Scrobbler::getListensTrackList(Database::Session& session, Database::User::pointer user) - { - return Utils::getListensTrackList(session, user); - } - void Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint) { diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp index c1b2406e..8134ac3c 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp @@ -49,11 +49,9 @@ namespace Scrobbling::ListenBrainz void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user) override; // Submit listens void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); - //std::optional createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint); boost::asio::io_context& _ioContext; Database::Db& _db; diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index c4216a33..644172a0 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -27,6 +27,7 @@ #include "services/database/Artist.hpp" #include "services/database/Db.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" @@ -87,10 +88,10 @@ namespace } } - Database::Track::pointer + Database::TrackId tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata) { - Database::Track::pointer track; + using namespace Database; // first try to get the associated track using MBIDs, and then fallback on names if (metadata.type("additional_info") == Wt::Json::Type::Object) @@ -98,30 +99,29 @@ namespace const Wt::Json::Object& additionalInfo = metadata.get("additional_info"); if (std::optional recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))}) { - const auto tracks {Database::Track::getByRecordingMBID(session, *recordingMBID)}; + const auto tracks {Track::findByRecordingMBID(session, *recordingMBID)}; // if duplicated files, do not record it (let the user correct its database) if (tracks.size() == 1) - track = tracks.front(); + return tracks.front()->getId(); } } - if (track) - return track; - // these fields are mandatory const std::string trackName {static_cast(metadata.get("track_name"))}; const std::string releaseName {static_cast(metadata.get("release_name"))}; - auto tracks {Database::Track::getByNameAndReleaseName(session, trackName, releaseName)}; - if (tracks.size() > 1) + auto tracks {Track::findByNameAndReleaseName(session, trackName, releaseName)}; + if (tracks.results.size() > 1) { - tracks.erase(std::remove_if(std::begin(tracks), std::end(tracks), - [&](const Database::Track::pointer track) + tracks.results.erase(std::remove_if(std::begin(tracks.results), std::end(tracks.results), + [&](const TrackId trackId) { + const Track::pointer track {Track::find(session, trackId)}; + if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty()) { - const auto& artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; - if (std::none_of(std::begin(artists), std::end(artists), [&](const Database::Artist::pointer& artist) { return artist->getName() == artistName; })) + const auto& artists {track->getArtists({TrackArtistLinkType::Artist})}; + if (std::none_of(std::begin(artists), std::end(artists), [&](const Artist::pointer& artist) { return artist->getName() == artistName; })) return true; } if (metadata.type("additional_info") == Wt::Json::Type::Object) @@ -145,13 +145,13 @@ namespace } return false; - }), std::end(tracks)); + }), std::end(tracks.results)); } - if (tracks.size() == 1) - track = tracks.front(); + if (tracks.results.size() == 1) + return tracks.results.front(); - return track; + return {}; } struct ParseGetListensResult @@ -198,8 +198,8 @@ namespace else if (listenedAt < result.oldestEntry) result.oldestEntry = listenedAt; - if (const Database::Track::pointer track {tryMatchListen(session, metadata)}) - result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt}); + if (Database::TrackId trackId {tryMatchListen(session, metadata)}; trackId.isValid()) + result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, trackId}, listenedAt}); } } catch (const Wt::WException& error) @@ -234,15 +234,18 @@ namespace Scrobbling::ListenBrainz auto transaction {session.createUniqueTransaction()}; - const Database::User::pointer user {Database::User::getById(session, listen.userId)}; + if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)) + return; + + const Database::User::pointer user {Database::User::find(session, listen.userId)}; if (!user) return; - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; if (!track) return; - Database::TrackListEntry::create(session, track, Utils::getOrCreateListensTrackList(session, user), listen.listenedAt); + Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); UserContext& context {getUserContext(listen.userId)}; if (context.listenCount) @@ -304,14 +307,14 @@ namespace Scrobbling::ListenBrainz assert(!isFetching()); - std::vector userIds; + Database::RangeResults userIds; { Database::Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - userIds = Database::User::getAllIds(_db.getTLSSession()); + userIds = Database::User::find(_db.getTLSSession(), Database::Range {}); } - for (const Database::UserId userId : userIds) + for (const Database::UserId userId : userIds.results) { if (Utils::getListenBrainzToken(_db.getTLSSession(), userId)) startGetListens(getUserContext(userId)); @@ -458,25 +461,22 @@ namespace Scrobbling::ListenBrainz auto transaction {session.createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(session, context.userId)}; + Database::User::pointer user {Database::User::find(session, context.userId)}; if (!user) return; - Database::TrackList::pointer tracklist {Utils::getOrCreateListensTrackList(session, user)}; - for (const TimedListen& listen : parseResult.matchedListens) { - const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)}; + const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; if (!track) continue; - if (!tracklist->getEntryByTrackAndDateTime(track, listen.listenedAt)) - { - context.importedListenCount++; - Database::TrackListEntry::create(session, track, tracklist, listen.listenedAt); - } + if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)) + continue; + + Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); + context.importedListenCount++; } } - } // namespace Scrobbling::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index 70efa377..84ed3a6a 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -24,7 +24,6 @@ #include #include #include -#include #include "services/database/Types.hpp" #include "services/scrobbling/Listen.hpp" diff --git a/src/libs/services/scrobbling/impl/listenbrainz/LovesSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/LovesSynchronizer.hpp new file mode 100644 index 00000000..0f7bf48f --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/LovesSynchronizer.hpp @@ -0,0 +1,92 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "services/database/Types.hpp" +#include "services/scrobbling/Listen.hpp" + +namespace Database +{ + class Db; + class Session; + class TrackList; + class User; +} + +namespace Scrobbling::ListenBrainz +{ + class ListensSynchronizer + { + public: + ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, std::string_view baseAPIUrl); + + void saveListen(const TimedListen& listen); + + private: + struct UserContext + { + UserContext(Database::UserId id) : userId {id} {} + + UserContext(const UserContext&) = delete; + UserContext(UserContext&&) = delete; + UserContext& operator=(const UserContext&) = delete; + UserContext& operator=(UserContext&&) = delete; + + const Database::UserId userId; + bool fetching {}; + + // resetted at each fetch + std::string listenBrainzUserName; // need to be resolved first + Wt::WDateTime maxDateTime; + std::size_t fetchedLoveCount{}; + std::size_t matchedLoveCount{}; + std::size_t importedLoveCount{}; + }; + + UserContext& getUserContext(Database::UserId userId); + bool isFetching() const; + void scheduleGetListens(std::chrono::seconds fromNow); + void startGetListens(); + void startGetListens(UserContext& context); + void onGetListensEnded(UserContext& context); + void enqueValidateToken(UserContext& context); + void enqueGetListenCount(UserContext& context); + void enqueGetListens(UserContext& context); + void processGetListensResponse(std::string_view body, UserContext& context); + + boost::asio::io_context& _ioContext; + boost::asio::io_context::strand _strand {_ioContext}; + Database::Db& _db; + std::string _baseAPIUrl; + boost::asio::steady_timer _getListensTimer {_ioContext}; + + std::unordered_map _userContexts; + + const std::size_t _maxSyncListenCount; + const std::chrono::hours _syncListensPeriod; + }; +} // Scrobbling::ListenBrainz + diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp index a2d9c9e1..e956370f 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.cpp @@ -34,7 +34,7 @@ namespace Scrobbling::ListenBrainz::Utils { auto transaction {session.createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(session, userId)}; + const Database::User::pointer user {Database::User::find(session, userId)}; if (!user) return std::nullopt; @@ -43,21 +43,4 @@ namespace Scrobbling::ListenBrainz::Utils return user->getListenBrainzToken(); } - - Database::TrackList::pointer - getListensTrackList(Database::Session& session, Database::User::pointer user) - { - return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user); - } - - Database::TrackList::pointer - getOrCreateListensTrackList(Database::Session& session, Database::User::pointer user) - { - Database::TrackList::pointer tracklist {getListensTrackList(session, user)}; - if (!tracklist) - tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user); - - return tracklist; - } - } diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp index aca140c7..4a4897c3 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp @@ -19,21 +19,18 @@ #pragma once -#include #include "utils/UUID.hpp" #include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace Database { class Session; - class TrackList; class User; } namespace Scrobbling::ListenBrainz::Utils { std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); - Database::ObjectPtr getOrCreateListensTrackList(Database::Session& session, Database::ObjectPtr user); - Database::ObjectPtr getListensTrackList(Database::Session& session, Database::ObjectPtr user); } diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index cf67d7fc..18b00129 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -24,21 +24,17 @@ #include #include #include -#include - -#include #include "services/scrobbling/Listen.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" namespace Database { - class Artist; class Db; - class Release; - class Session; - class Track; - class User; } namespace Scrobbling @@ -56,45 +52,56 @@ namespace Scrobbling virtual void addTimedListen(const TimedListen& listen) = 0; // Stats - template - using ResultContainer = std::vector; - - using ArtistContainer = ResultContainer; - using ReleaseContainer = ResultContainer; - using TrackContainer = ResultContainer; + using ArtistContainer = Database::RangeResults; + using ReleaseContainer = Database::RangeResults; + using TrackContainer = Database::RangeResults; // From most recent to oldest virtual ArtistContainer getRecentArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, - std::optional range, - bool& moreResults) = 0; + Database::Range range) = 0; virtual ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; + Database::Range range) = 0; virtual TrackContainer getRecentTracks(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; + Database::Range range) = 0; // Top virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, - std::optional range, - bool& moreResults) = 0; + Database::Range) = 0; virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; + Database::Range range) = 0; virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector& clusterIds, - std::optional range, - bool& moreResults) = 0; + Database::Range range) = 0; + + // Star + virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; + virtual ArtistContainer getStarredArtists(Database::UserId userId, + const std::vector& clusterIds, + std::optional linkType, + Database::ArtistSortMethod sortMethod, + Database::Range range) = 0; + + virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; + virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; + virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; + virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + + virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; + virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; + virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; + virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; }; std::unique_ptr createScrobblingService(boost::asio::io_service& ioService, Database::Db& db); diff --git a/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp b/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp index 50029c99..a42ab649 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/Listen.hpp @@ -21,7 +21,8 @@ #include -#include "services/database/Types.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/UserId.hpp" namespace Scrobbling { diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index c36bf1ba..d4f4b985 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -20,7 +20,6 @@ target_link_libraries(lmssubsonic PRIVATE lmsauth lmsav lmsdatabase - lmsfeedback lmsrecommendation lmsscanner lmsscrobbling diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index 982963ea..01930783 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -23,7 +23,7 @@ #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" #include "ClientInfo.hpp" #include "ProtocolVersion.hpp" diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index fe4218d0..8089779c 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -74,7 +74,7 @@ getStreamParameters(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; { - auto track {Track::getById(context.dbSession, id)}; + auto track {Track::find(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; @@ -82,7 +82,7 @@ getStreamParameters(RequestContext& context) } { - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -124,7 +124,7 @@ handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Ht { auto transaction {context.dbSession.createSharedTransaction()}; - auto track {Track::getById(context.dbSession, id)}; + auto track {Track::find(context.dbSession, id)}; if (!track) throw RequestedDataNotFoundError {}; diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index dcc2e50b..32b45715 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -19,7 +19,10 @@ #pragma once -#include "services/database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/TrackListId.hpp" #include "utils/String.hpp" namespace API::Subsonic diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index dc6440f9..54e53642 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -37,7 +37,6 @@ #include "services/database/TrackBookmark.hpp" #include "services/database/TrackList.hpp" #include "services/database/User.hpp" -#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "services/cover/ICoverService.hpp" @@ -164,7 +163,7 @@ static void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) { - User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + User::pointer currentUser {User::find(context.dbSession, context.userId)}; if (!currentUser) throw RequestedDataNotFoundError {}; @@ -178,7 +177,7 @@ checkUserTypeIsAllowed(RequestContext& context, EnumSet allo { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + User::pointer currentUser {User::find(context.dbSession, context.userId)}; if (!currentUser) throw RequestedDataNotFoundError {}; @@ -323,11 +322,11 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", dateTimeToCreatedString(track->getLastWritten())); - if (Service::get()->isStarred(user->getId(), track->getId())) + if (Service::get()->isStarred(user->getId(), track->getId())) trackResponse.setAttribute("starred", reportedStarredDate); // Report the first GENRE for this track - ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::find(dbSession, genreClusterName)}; if (clusterType) { auto clusters {track->getClusterGroups({clusterType}, 1)}; @@ -405,7 +404,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const if (id3) { // Report the first GENRE for this track - ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::find(dbSession, genreClusterName)}; if (clusterType) { auto clusters {release->getClusterGroups({clusterType}, 1)}; @@ -414,7 +413,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const } } - if (Service::get()->isStarred(user->getId(), release->getId())) + if (Service::get()->isStarred(user->getId(), release->getId())) albumNode.setAttribute("starred", reportedStarredDate); return albumNode; @@ -432,7 +431,7 @@ artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, b if (id3) artistNode.setAttribute("albumCount", artist->getReleaseCount()); - if (Service::get()->isStarred(user->getId(), artist->getId())) + if (Service::get()->isStarred(user->getId(), artist->getId())) artistNode.setAttribute("starred", reportedStarredDate); return artistNode; @@ -500,7 +499,7 @@ handleChangePassword(RequestContext& context) checkUserIsMySelfOrAdmin(context, username); - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw UserNotAuthorizedError {}; @@ -540,14 +539,14 @@ handleCreatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; TrackList::pointer tracklist; if (id) { - tracklist = TrackList::getById(context.dbSession, *id); + tracklist = TrackList::find(context.dbSession, *id); if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -565,11 +564,11 @@ handleCreatePlaylistRequest(RequestContext& context) for (const TrackId trackId : trackIds) { - Track::pointer track {Track::getById(context.dbSession, trackId)}; + Track::pointer track {Track::find(context.dbSession, trackId)}; if (!track) continue; - TrackListEntry::create(context.dbSession, track, tracklist ); + TrackListEntry::create(context.dbSession, track, tracklist); } return Response::createOkResponse(context.serverProtocolVersion); @@ -587,7 +586,7 @@ handleCreateUserRequest(RequestContext& context) { auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (user) throw UserAlreadyExistsGenericError {}; @@ -598,7 +597,7 @@ handleCreateUserRequest(RequestContext& context) auto removeCreatedUser {[&]() { auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, userId)}; + User::pointer user {User::find(context.dbSession, userId)}; if (user) user.remove(); }}; @@ -634,11 +633,11 @@ handleDeletePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -659,7 +658,7 @@ handleDeleteUserRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -696,17 +695,20 @@ handleGetRandomSongsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - auto tracks {Track::getAllRandom(context.dbSession, {}, size)}; + const auto trackIds {Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size}))}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; - for (const Track::pointer& track : tracks) + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(context.dbSession, trackId)}; randomSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } return response; } @@ -724,78 +726,86 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) const Range range {offset, size}; - std::vector releases; + RangeResults releases; Scrobbling::IScrobblingService& scrobbling {*Service::get()}; auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; if (type == "alphabeticalByName") { - releases = Release::getAll(context.dbSession, range); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } else if (type == "alphabeticalByArtist") { - releases = Release::getAllOrderedByArtist(context.dbSession, offset, size); + releases = Release::findOrderedByArtist(context.dbSession, range); } else if (type == "byGenre") { // Mandatory param - std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; + const std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; - ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; - if (clusterType) + if (const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)}) { - Cluster::pointer cluster {clusterType->getCluster(genre)}; - if (cluster) + if (const Cluster::pointer cluster {clusterType->getCluster(genre)}) { - bool more; - releases = Release::getByFilter(context.dbSession, {cluster->getId()}, {}, range, more); + Release::FindParameters params; + params.setClusters({cluster->getId()}); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } } } else if (type == "byYear") { - int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; - int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; + const int fromYear {getMandatoryParameterAs(context.parameters, "fromYear")}; + const int toYear {getMandatoryParameterAs(context.parameters, "toYear")}; - releases = Release::getByYear(context.dbSession, fromYear, toYear, range); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Date); + params.setRange(range); + params.setDateRange(DateRange::fromYearRange(fromYear, toYear)); + + releases = Release::find(context.dbSession, params); } else if (type == "frequent") { - bool moreResults {}; - for (ReleaseId releaseId : scrobbling.getTopReleases(context.userId, {}, range, moreResults)) - { - if (Release::pointer release {Release::getById(context.dbSession, releaseId)}) - releases.push_back(release ); - } + releases = scrobbling.getTopReleases(context.userId, {}, range); } else if (type == "newest") { - bool moreResults {}; - releases = Release::getLastWritten(context.dbSession, std::nullopt, {}, range, moreResults); + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); + + releases = Release::find(context.dbSession, params); } else if (type == "random") { // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums - releases = Release::getAllRandom(context.dbSession, {}, size); + // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) + Release::FindParameters params; + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({0, size}); + + releases = Release::find(context.dbSession, params); } else if (type == "recent") { - bool moreResults {}; - for (ReleaseId releaseId : scrobbling.getRecentReleases(context.userId, {}, range, moreResults)) - { - if (Release::pointer release {Release::getById(context.dbSession, releaseId)}) - releases.push_back(release ); - } + releases = scrobbling.getRecentReleases(context.userId, {}, range); } else if (type == "starred") { - bool moreResults {}; - releases = Release::getStarred(context.dbSession, user, {}, range, moreResults); + releases = scrobbling.getStarredReleases(context.userId, {}, range); } else throw NotImplementedGenericError {}; @@ -803,8 +813,11 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; - for (const Release::pointer& release : releases) + for (const ReleaseId releaseId : releases.results) + { + const Release::pointer release {Release::find(context.dbSession, releaseId)}; albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + } return response; } @@ -832,11 +845,11 @@ handleGetAlbumRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Release::pointer release {Release::getById(context.dbSession, id)}; + Release::pointer release {Release::find(context.dbSession, id)}; if (!release) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -861,11 +874,11 @@ handleGetArtistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id)}; + const Artist::pointer artist {Artist::find(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -897,7 +910,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - Artist::pointer artist {Artist::getById(context.dbSession, id)}; + const Artist::pointer artist {Artist::find(context.dbSession, id)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -911,13 +924,13 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - for ( const ArtistId similarArtistId : similarArtistsId ) + for (const ArtistId similarArtistId : similarArtistsId) { - Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)}; if (similarArtist) artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3)); } @@ -955,32 +968,31 @@ handleGetArtistsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + Artist::FindParameters findParameters; + + findParameters.setSortMethod(ArtistSortMethod::BySortName); switch (user->getSubsonicArtistListMode()) { - case User::SubsonicArtistListMode::AllArtists: + case SubsonicArtistListMode::AllArtists: break; - case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLinkType::ReleaseArtist; + case SubsonicArtistListMode::ReleaseArtists: + findParameters.setLinkType(TrackArtistLinkType::ReleaseArtist); break; - case User::SubsonicArtistListMode::TrackArtists: - linkType = TrackArtistLinkType::Artist; + case SubsonicArtistListMode::TrackArtists: + findParameters.setLinkType(TrackArtistLinkType::Artist); break; } - bool more {}; - const std::vector artists {Artist::getByFilter(context.dbSession, - {}, - {}, - linkType, - Artist::SortMethod::BySortName, - std::nullopt, more)}; - for (const Artist::pointer& artist : artists) + const RangeResults artists {Artist::find(context.dbSession, findParameters)}; + for (const ArtistId artistId : artists.results) + { + const auto artist {Artist::find(context.dbSession, artistId)}; indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */)); + } return response; } @@ -1003,7 +1015,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1012,16 +1024,18 @@ handleGetMusicDirectoryRequest(RequestContext& context) directoryNode.setAttribute("id", idToString(RootId {})); directoryNode.setAttribute("name", "Music"); - bool moreResults{}; - auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; - for (const Artist::pointer& artist : artists) + auto artistIds {Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))}; + for (const ArtistId artistId : artistIds.results) + { + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); + } } else if (artistId) { directoryNode.setAttribute("id", idToString(*artistId)); - auto artist {Artist::getById(context.dbSession, *artistId)}; + auto artist {Artist::find(context.dbSession, *artistId)}; if (!artist) throw RequestedDataNotFoundError {}; @@ -1035,7 +1049,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) { directoryNode.setAttribute("id", idToString(*releaseId)); - auto release {Release::getById(context.dbSession, *releaseId)}; + auto release {Release::find(context.dbSession, *releaseId)}; if (!release) throw RequestedDataNotFoundError {}; @@ -1075,7 +1089,7 @@ handleGetGenresRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - const ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; + const ClusterType::pointer clusterType {ClusterType::find(context.dbSession, genreClusterName)}; if (clusterType) { const auto clusters {clusterType->getClusters()}; @@ -1102,32 +1116,30 @@ handleGetIndexesRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - std::optional linkType; + Artist::FindParameters parameters; + parameters.setSortMethod(ArtistSortMethod::BySortName); switch (user->getSubsonicArtistListMode()) { - case User::SubsonicArtistListMode::AllArtists: + case SubsonicArtistListMode::AllArtists: break; - case User::SubsonicArtistListMode::ReleaseArtists: - linkType = TrackArtistLinkType::ReleaseArtist; + case SubsonicArtistListMode::ReleaseArtists: + parameters.setLinkType(TrackArtistLinkType::ReleaseArtist); break; - case User::SubsonicArtistListMode::TrackArtists: - linkType = TrackArtistLinkType::Artist; + case SubsonicArtistListMode::TrackArtists: + parameters.setLinkType(TrackArtistLinkType::Artist); break; } - bool more {}; - const std::vector artists {Artist::getByFilter(context.dbSession, - {}, - {}, - linkType, - Artist::SortMethod::BySortName, - std::nullopt, more)}; - for (const Artist::pointer& artist : artists) + const RangeResults artists {Artist::find(context.dbSession, parameters)}; + for (const ArtistId artistId : artists.results) + { + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */)); + } return response; } @@ -1146,11 +1158,11 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - const Artist::pointer artist {Artist::getById(context.dbSession, artistId)}; + const Artist::pointer artist {Artist::find(context.dbSession, artistId)}; if (!artist) throw RequestedDataNotFoundError {}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1158,7 +1170,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) auto tracks {artist->getRandomTracks(count / 2)}; for (const ArtistId similarArtistId : similarArtistIds) { - const Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + const Artist::pointer similarArtist {Artist::find(context.dbSession, similarArtistId)}; if (!similarArtist) continue; @@ -1199,31 +1211,30 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; + Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + + for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range {}).results) { - bool moreResults {}; - const auto artists {Artist::getStarred(context.dbSession, user, {}, std::nullopt, Artist::SortMethod::BySortName, std::nullopt, moreResults)}; - for (const Artist::pointer& artist : artists) + if (auto artist {Artist::find(context.dbSession, artistId)}) starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } + for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range {}).results) { - bool moreResults {}; - const auto releases {Release::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)}; - for (const Release::pointer& release : releases) + if (auto release {Release::find(context.dbSession, releaseId)}) starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } + for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range {}).results) { - bool moreResults {}; - const auto tracks {Track::getStarred(context.dbSession, user, {}, std::nullopt, moreResults)}; - for (const Track::pointer& track : tracks) + if (auto track {Track::find(context.dbSession, trackId)}) starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } @@ -1271,11 +1282,11 @@ handleGetPlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, trackListId)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, trackListId)}; if (!tracklist) throw RequestedDataNotFoundError {}; @@ -1297,16 +1308,15 @@ handleGetPlaylistsRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& playlistsNode {response.createNode("playlists")}; - auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; - for (const TrackList::pointer& tracklist : tracklists) - playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.dbSession)); + auto tracklistIds {TrackList::find(context.dbSession, context.userId, TrackList::Type::Playlist, Range {})}; + for (const TrackListId trackListId : tracklistIds.results) + { + const TrackList::pointer trackList {TrackList::find(context.dbSession, trackListId)}; + playlistsNode.addArrayChild("playlist", tracklistToResponseNode(trackList, context.dbSession)); + } return response; } @@ -1326,7 +1336,7 @@ handleGetSongsByGenreRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; + auto clusterType {ClusterType::find(context.dbSession, genreClusterName)}; if (!clusterType) throw RequestedDataNotFoundError {}; @@ -1334,17 +1344,23 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!cluster) throw RequestedDataNotFoundError {}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; - bool more; - auto tracks {Track::getByFilter(context.dbSession, {cluster->getId()}, {}, Range {offset, size}, more)}; - for (const Track::pointer& track : tracks) + Track::FindParameters params; + params.setClusters({cluster->getId()}); + params.setRange({offset, size}); + + auto trackIds {Track::find(context.dbSession, params)}; + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(context.dbSession, trackId)}; songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } return response; } @@ -1359,7 +1375,7 @@ handleGetUserRequest(RequestContext& context) checkUserIsMySelfOrAdmin(context, username); - const User::pointer user {User::getByLoginName(context.dbSession, username)}; + const User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1378,9 +1394,12 @@ handleGetUsersRequest(RequestContext& context) Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& usersNode {response.createNode("users")}; - const auto users {User::getAll(context.dbSession)}; - for (const User::pointer& user : users) + const auto userIds {User::find(context.dbSession, Range {})}; + for (const UserId userId : userIds.results) + { + const User::pointer user {User::find(context.dbSession, userId)}; usersNode.addArrayChild("user", userToResponseNode(user)); + } return response; } @@ -1404,30 +1423,52 @@ handleSearchRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; - bool more; { - auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, Range {artistOffset, artistCount}, more)}; - for (const Artist::pointer& artist : artists) + Artist::FindParameters params; + params.setKeywords(keywords); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange({artistOffset, artistCount}); + + RangeResults artistIds {Artist::find(context.dbSession, params)}; + for (const ArtistId artistId : artistIds.results) + { + const auto artist {Artist::find(context.dbSession, artistId)}; searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3)); + } } { - auto releases {Release::getByFilter(context.dbSession, {}, keywords, Range {albumOffset, albumCount}, more)}; - for (const Release::pointer& release : releases) + Release::FindParameters params; + params.setKeywords(keywords); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange({albumOffset, albumCount}); + + RangeResults releaseIds {Release::find(context.dbSession, params)}; + for (const ReleaseId releaseId : releaseIds.results) + { + const auto release {Release::find(context.dbSession, releaseId)}; searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); + } } { - auto tracks {Track::getByFilter(context.dbSession, {}, keywords, Range {songOffset, songCount}, more)}; - for (const Track::pointer& track : tracks) + Track::FindParameters params; + params.setKeywords(keywords); + params.setRange({songOffset, songCount}); + + RangeResults trackIds {Track::find(context.dbSession, params)}; + for (const TrackId trackId : trackIds.results) + { + const auto track {Track::find(context.dbSession, trackId)}; searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); + } } return response; @@ -1462,35 +1503,35 @@ handleStarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; for (const ArtistId id : params.artistIds) { - Artist::pointer artist {Artist::getById(context.dbSession, id)}; + Artist::pointer artist {Artist::find(context.dbSession, id)}; if (!artist) continue; - Service::get()->star(user->getId(), artist->getId()); + Service::get()->star(user->getId(), artist->getId()); } for (const ReleaseId id : params.releaseIds) { - Release::pointer release {Release::getById(context.dbSession, id)}; + Release::pointer release {Release::find(context.dbSession, id)}; if (!release) continue; - Service::get()->star(user->getId(), release->getId()); + Service::get()->star(user->getId(), release->getId()); } for (const TrackId id : params.trackIds) { - Track::pointer track {Track::getById(context.dbSession, id)}; + Track::pointer track {Track::find(context.dbSession, id)}; if (!track) continue; - Service::get()->star(user->getId(), track->getId()); + Service::get()->star(user->getId(), track->getId()); } return Response::createOkResponse(context.serverProtocolVersion); @@ -1518,35 +1559,29 @@ handleUnstarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw RequestedDataNotFoundError {}; - for (const ArtistId id : params.artistIds) - { - Artist::pointer artist {Artist::getById(context.dbSession, id)}; - if (!artist) - continue; - - Service::get()->unstar(user->getId(), artist->getId()); - } + for (const ArtistId artistId : params.artistIds) + Service::get()->unstar(context.userId, artistId); for (const ReleaseId id : params.releaseIds) { - Release::pointer release {Release::getById(context.dbSession, id)}; + Release::pointer release {Release::find(context.dbSession, id)}; if (!release) continue; - Service::get()->unstar(user->getId(), release->getId()); + Service::get()->unstar(user->getId(), release->getId()); } for (const TrackId id : params.trackIds) { - Track::pointer track {Track::getById(context.dbSession, id)}; + Track::pointer track {Track::find(context.dbSession, id)}; if (!track) continue; - Service::get()->unstar(user->getId(), track->getId()); + Service::get()->unstar(user->getId(), track->getId()); } @@ -1608,7 +1643,7 @@ handleUpdateUserRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; + User::pointer user {User::find(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1656,11 +1691,11 @@ handleUpdatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - TrackList::pointer tracklist {TrackList::getById(context.dbSession, id)}; + TrackList::pointer tracklist {TrackList::find(context.dbSession, id)}; if (!tracklist || tracklist->getUser() != user || tracklist->getType() != TrackList::Type::Playlist) @@ -1689,7 +1724,7 @@ handleUpdatePlaylistRequest(RequestContext& context) // Add tracks for (const TrackId trackIdToAdd : trackIdsToAdd) { - Track::pointer track {Track::getById(context.dbSession, trackIdToAdd)}; + Track::pointer track {Track::find(context.dbSession, trackIdToAdd)}; if (!track) continue; @@ -1705,17 +1740,18 @@ handleGetBookmarks(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getById(context.dbSession, context.userId)}; + User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)}; + const auto bookmarkIds {TrackBookmark::find(context.dbSession, user->getId(), Range {})}; Response response {Response::createOkResponse(context.serverProtocolVersion)}; Response::Node& bookmarksNode {response.createNode("bookmarks")}; - for (const TrackBookmark::pointer& bookmark : bookmarks) + for (const TrackBookmarkId bookmarkId : bookmarkIds.results) { + const TrackBookmark::pointer bookmark {TrackBookmark::find(context.dbSession, bookmarkId)}; Response::Node bookmarkNode {trackBookmarkToResponseNode(bookmark)}; bookmarkNode.addArrayChild("entry", trackToResponseNode(bookmark->getTrack(), context.dbSession, user)); @@ -1730,22 +1766,22 @@ Response handleCreateBookmark(RequestContext& context) { // Mandatory params - TrackId id {getMandatoryParameterAs(context.parameters, "id")}; + TrackId trackId {getMandatoryParameterAs(context.parameters, "id")}; unsigned long position {getMandatoryParameterAs(context.parameters, "position")}; const std::optional comment {getParameterAs(context.parameters, "comment")}; auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; + const User::pointer user {User::find(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; - const Track::pointer track {Track::getById(context.dbSession, id)}; + const Track::pointer track {Track::find(context.dbSession, trackId)}; if (!track) throw RequestedDataNotFoundError {}; // Replace any existing bookmark - auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)}; + auto bookmark {TrackBookmark::find(context.dbSession, user->getId(), trackId)}; if (!bookmark) bookmark = TrackBookmark::create(context.dbSession, user, track); @@ -1761,19 +1797,11 @@ Response handleDeleteBookmark(RequestContext& context) { // Mandatory params - TrackId id {getMandatoryParameterAs(context.parameters, "id")}; + TrackId trackId {getMandatoryParameterAs(context.parameters, "id")}; auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getById(context.dbSession, context.userId)}; - if (!user) - throw UserNotAuthorizedError {}; - - const Track::pointer track {Track::getById(context.dbSession, id)}; - if (!track) - throw RequestedDataNotFoundError {}; - - auto bookmark {TrackBookmark::getByUser(context.dbSession, user, track)}; + auto bookmark {TrackBookmark::find(context.dbSession, context.userId, trackId)}; if (!bookmark) throw RequestedDataNotFoundError {}; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 83bead84..33dad4b1 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -52,7 +52,6 @@ target_link_libraries(lms PRIVATE lmsav lmsauth lmsdatabase - lmsfeedback lmsrecommendation lmsscanner lmsscrobbling diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 8329f6d6..c269f7a8 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -31,7 +31,6 @@ #include "services/cover/ICoverService.hpp" #include "services/database/Db.hpp" #include "services/database/Session.hpp" -#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" #include "services/scrobbling/IScrobblingService.hpp" @@ -239,7 +238,6 @@ int main(int argc, char* argv[]) // Service initialization order is important (reverse-order for deinit) Service childProcessManagerService {createChildProcessManager(ioContext)}; - Service feedbackService {Feedback::createFeedbackService(database)}; Service authTokenService; Service authPasswordService; Service authEnvService; diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index b505d8af..2db329bc 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -109,7 +109,7 @@ class AuthModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; + Database::User::pointer user {Database::User::find(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); _userId = user->getId(); @@ -205,7 +205,7 @@ Auth::Auth() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::User::pointer demoUser {Database::User::getDemo(LmsApp->getDbSession())}; + Database::User::pointer demoUser {Database::User::findDemoUser(LmsApp->getDbSession())}; if (demoUser) { model->setValue(AuthModel::LoginNameField, demoUser->getLoginName()); diff --git a/src/lms/ui/Auth.hpp b/src/lms/ui/Auth.hpp index 87c00228..305e9a4d 100644 --- a/src/lms/ui/Auth.hpp +++ b/src/lms/ui/Auth.hpp @@ -19,9 +19,10 @@ #pragma once +#include #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace UserInterface { diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index a7132699..c15f05f1 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -101,7 +101,7 @@ LmsApplication::getUser() if (!_authenticatedUser) return {}; - return Database::User::getById(getDbSession(), _authenticatedUser->userId); + return Database::User::find(getDbSession(), _authenticatedUser->userId); } Database::UserId @@ -247,7 +247,7 @@ LmsApplication::processPasswordAuth() void LmsApplication::setTheme() { - Database::User::UITheme theme {Database::User::defaultUITheme}; + Database::UITheme theme {Database::User::defaultUITheme}; { auto transaction {getDbSession().createSharedTransaction()}; if (const auto user {getUser()}) diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 6c32fffe..080c03f9 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -23,6 +23,9 @@ #include +#include "services/database/Object.hpp" +#include "services/database/UserId.hpp" +#include "services/database/Types.hpp" #include "services/scanner/ScannerEvents.hpp" namespace Database diff --git a/src/lms/ui/LmsApplicationManager.hpp b/src/lms/ui/LmsApplicationManager.hpp index f478ae8d..5a81aec1 100644 --- a/src/lms/ui/LmsApplicationManager.hpp +++ b/src/lms/ui/LmsApplicationManager.hpp @@ -25,7 +25,7 @@ #include -#include "services/database/Types.hpp" +#include "services/database/UserId.hpp" namespace UserInterface { diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index d56cbe5a..e5d9f3cc 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -24,7 +24,7 @@ namespace UserInterface { -LmsTheme::LmsTheme(Database::User::UITheme theme) +LmsTheme::LmsTheme(Database::UITheme theme) : _theme {theme} { setVersion(Wt::BootstrapVersion::v3); @@ -32,7 +32,7 @@ LmsTheme::LmsTheme(Database::User::UITheme theme) } void -LmsTheme::setTheme(Database::User::UITheme theme) +LmsTheme::setTheme(Database::UITheme theme) { if (theme == _theme) return; @@ -59,11 +59,11 @@ LmsTheme::styleSheets() const } std::vector -LmsTheme::getStyleSheets(Database::User::UITheme theme) +LmsTheme::getStyleSheets(Database::UITheme theme) { switch (theme) { - case Database::User::UITheme::Dark: + case Database::UITheme::Dark: return { {"css/fonts.css"}, @@ -73,7 +73,7 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) {"css/lms-darkly.css"}, }; - case Database::User::UITheme::Light: + case Database::UITheme::Light: return { {"css/fonts.css"}, diff --git a/src/lms/ui/LmsTheme.hpp b/src/lms/ui/LmsTheme.hpp index dce55425..912137a6 100644 --- a/src/lms/ui/LmsTheme.hpp +++ b/src/lms/ui/LmsTheme.hpp @@ -24,21 +24,21 @@ #include #include -#include "services/database/User.hpp" +#include "services/database/Types.hpp" namespace UserInterface { class LmsTheme : public Wt::WBootstrapTheme { public: - LmsTheme(Database::User::UITheme theme); + LmsTheme(Database::UITheme theme); - void setTheme(Database::User::UITheme theme); + void setTheme(Database::UITheme theme); private: std::vector styleSheets() const override; - static std::vector getStyleSheets(Database::User::UITheme theme); + static std::vector getStyleSheets(Database::UITheme theme); - Database::User::UITheme _theme; + Database::UITheme _theme; }; } diff --git a/src/lms/ui/MediaPlayer.cpp b/src/lms/ui/MediaPlayer.cpp index eefd44fc..43f37e76 100644 --- a/src/lms/ui/MediaPlayer.cpp +++ b/src/lms/ui/MediaPlayer.cpp @@ -119,10 +119,10 @@ bitrateFromString(const std::string& str) if (!value) return std::nullopt; - if (Database::User::audioTranscodeAllowedBitrates.find(*value) != std::cend(Database::User::audioTranscodeAllowedBitrates)) - return *value; + if (!Database::isAudioBitrateAllowed(*value)) + return std::nullopt; - return std::nullopt; + return *value; } static @@ -241,7 +241,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + const auto track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) return; diff --git a/src/lms/ui/MediaPlayer.hpp b/src/lms/ui/MediaPlayer.hpp index 542cc081..40307c4b 100644 --- a/src/lms/ui/MediaPlayer.hpp +++ b/src/lms/ui/MediaPlayer.hpp @@ -27,6 +27,7 @@ #include #include +#include "services/database/TrackId.hpp" #include "services/database/Types.hpp" namespace UserInterface { diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index fd4f42b2..0e88bd84 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -28,7 +28,7 @@ #include "services/database/Track.hpp" #include "services/database/TrackList.hpp" #include "services/database/User.hpp" -#include "services/feedback/IFeedbackService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/Random.hpp" @@ -132,7 +132,7 @@ PlayQueue::PlayQueue() if (LmsApp->getUser()->isDemo()) { LMS_LOG(UI, DEBUG) << "Removing tracklist id " << _tracklistId.toString(); - auto tracklist = Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); + auto tracklist = Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); if (tracklist) tracklist.remove(); } @@ -161,7 +161,11 @@ PlayQueue::PlayQueue() loadTrack(trackPos, false); }); - trackList = LmsApp->getUser()->getQueuedTrackList(LmsApp->getDbSession()); + + static const std::string queuedListName {"__queued_tracks__"}; + trackList = Database::TrackList::find(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, LmsApp->getUserId()); + if (!trackList) + trackList = Database::TrackList::create(LmsApp->getDbSession(), queuedListName, Database::TrackList::Type::Internal, false, LmsApp->getUser()); } else { @@ -193,7 +197,7 @@ PlayQueue::updateRadioBtn() Database::TrackList::pointer PlayQueue::getTrackList() const { - return Database::TrackList::getById(LmsApp->getDbSession(), _tracklistId); + return Database::TrackList::find(LmsApp->getDbSession(), _tracklistId); } bool @@ -327,7 +331,7 @@ PlayQueue::enqueueTracks(const std::vector& trackIds) std::size_t nbTracksToEnqueue {tracklist->getCount() + trackIds.size() > _nbMaxEntries ? _nbMaxEntries - tracklist->getCount() : trackIds.size()}; for (const Database::TrackId trackId : trackIds) { - Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) continue; @@ -489,14 +493,14 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) { Wt::WPopupMenu* popup {LmsApp->createPopupMenu()}; - const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(moreBtn, [=] { if (isStarred) - Service::get()->unstar(LmsApp->getUserId(), trackId); + Service::get()->unstar(LmsApp->getUserId(), trackId); else - Service::get()->star(LmsApp->getUserId(), trackId); + Service::get()->star(LmsApp->getUserId(), trackId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(trackId)}); @@ -508,7 +512,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) void PlayQueue::enqueueRadioTracks() { - const auto similarTrackIds {Service::get()->getSimilarTracksFromTrackList(_tracklistId, 3)}; + const auto similarTrackIds {Service::get()->findSimilarTracksFromTrackList(_tracklistId, 3)}; std::vector trackToAddIds(std::cbegin(similarTrackIds), std::cend(similarTrackIds)); Random::shuffleContainer(trackToAddIds); diff --git a/src/lms/ui/PlayQueue.hpp b/src/lms/ui/PlayQueue.hpp index 89f15ff4..b90b28ce 100644 --- a/src/lms/ui/PlayQueue.hpp +++ b/src/lms/ui/PlayQueue.hpp @@ -25,7 +25,8 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" +#include "services/database/TrackListId.hpp" #include "PlayQueueAction.hpp" namespace Similarity diff --git a/src/lms/ui/PlayQueueAction.hpp b/src/lms/ui/PlayQueueAction.hpp index 4fa8b426..c2a45b34 100644 --- a/src/lms/ui/PlayQueueAction.hpp +++ b/src/lms/ui/PlayQueueAction.hpp @@ -22,7 +22,9 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" namespace UserInterface { diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 1bc35013..7bbc5672 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -36,6 +36,7 @@ #include "services/auth/IPasswordService.hpp" #include "services/database/Session.hpp" +#include "services/database/User.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -139,7 +140,7 @@ class SettingsModel : public Wt::WFormModel User::pointer user {LmsApp->getUser()}; { - const User::UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? User::UITheme::Dark : User::UITheme::Light}; + const UITheme newTheme {Wt::asNumber(value(DarkModeField)) ? UITheme::Dark : UITheme::Light}; LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; lmsTheme->setTheme(newTheme); @@ -208,7 +209,7 @@ class SettingsModel : public Wt::WFormModel User::pointer user {LmsApp->getUser()}; - setValue(DarkModeField, user->getUITheme() == User::UITheme::Dark); + setValue(DarkModeField, user->getUITheme() == UITheme::Dark); { const auto settings {*LmsApp->getMediaPlayer().getSettings()}; @@ -333,10 +334,10 @@ class SettingsModel : public Wt::WFormModel _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); _transcodeBitrateModel = std::make_shared>(); - for (const Bitrate bitrate : User::audioTranscodeAllowedBitrates) + visitAllowedAudioBitrates([&](const Bitrate bitrate) { _transcodeBitrateModel->add(Wt::WString::fromUTF8(std::to_string(bitrate / 1000)), bitrate); - } + }); _transcodeFormatModel = std::make_shared>(); _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.mp3"), AudioFormat::MP3); @@ -351,10 +352,10 @@ class SettingsModel : public Wt::WFormModel _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.track"), MediaPlayer::Settings::ReplayGain::Mode::Track); _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.release"), MediaPlayer::Settings::ReplayGain::Mode::Release); - _subsonicArtistListModeModel = std::make_shared>(); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), User::SubsonicArtistListMode::AllArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), User::SubsonicArtistListMode::ReleaseArtists); - _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists); + _subsonicArtistListModeModel = std::make_shared>(); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.all-artists"), SubsonicArtistListMode::AllArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), SubsonicArtistListMode::ReleaseArtists); + _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), SubsonicArtistListMode::TrackArtists); _scrobblerModel = std::make_shared>(); _scrobblerModel->add(Wt::WString::tr("Lms.Settings.scrobbling.scrobbler.internal"), Scrobbler::Internal); @@ -368,7 +369,7 @@ class SettingsModel : public Wt::WFormModel std::shared_ptr> _transcodeBitrateModel; std::shared_ptr> _transcodeFormatModel; std::shared_ptr _replayGainModeModel; - std::shared_ptr> _subsonicArtistListModeModel; + std::shared_ptr> _subsonicArtistListModeModel; std::shared_ptr _scrobblerModel; }; diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index 4e8186b8..efb14db4 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -65,7 +65,7 @@ class InitWizardModel : public Wt::WFormModel // Check if a user already exist // If it's the case, just do nothing - if (!Database::User::getAll(LmsApp->getDbSession()).empty()) + if (Database::User::getCount(LmsApp->getDbSession()) > 0) throw LmsException {"Admin user already created"}; Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 12bf9392..764d6bb8 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -90,7 +90,7 @@ class ReportResource : public Wt::WResource for (const auto& duplicate : _stats.duplicates) { - const auto& track {Database::Track::getById(LmsApp->getDbSession(), duplicate.trackId)}; + const auto& track {Database::Track::find(LmsApp->getDbSession(), duplicate.trackId)}; if (!track) continue; diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index fd456993..218526f1 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -53,7 +53,7 @@ class UserModel : public Wt::WFormModel static inline const Field PasswordField {"password"}; static inline const Field DemoField {"demo"}; - UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) + UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) : _userId {userId} , _authPasswordService {authPasswordService} { @@ -82,7 +82,7 @@ class UserModel : public Wt::WFormModel if (_userId) { // Update user - Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; if (!user) throw UserNotFoundException {}; @@ -92,15 +92,15 @@ class UserModel : public Wt::WFormModel else { // Check races with other endpoints (subsonic API...) - Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + User::pointer user {User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; if (user) throw UserNotAllowedException {}; // Create user - user = Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); + user = User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); if (Wt::asNumber(value(DemoField))) - user.modify()->setType(Database::UserType::DEMO); + user.modify()->setType(UserType::DEMO); if (_authPasswordService) _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); @@ -115,20 +115,20 @@ class UserModel : public Wt::WFormModel auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; if (!user) throw UserNotFoundException {}; else if (user == LmsApp->getUser()) throw UserNotAllowedException {}; } - Database::UserType getUserType() const + UserType getUserType() const { if (_userId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; return user->getType(); } @@ -141,7 +141,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *_userId)}; return user->getLoginName(); } @@ -156,7 +156,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + const User::pointer user {User::find(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; if (user) error = Wt::WString::tr("Lms.Admin.User.user-already-exists"); } @@ -164,7 +164,7 @@ class UserModel : public Wt::WFormModel { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (Wt::asNumber(value(DemoField)) && Database::User::getDemo(LmsApp->getDbSession())) + if (Wt::asNumber(value(DemoField)) && User::findDemoUser(LmsApp->getDbSession())) error = Wt::WString::tr("Lms.Admin.User.demo-account-already-exists"); } @@ -176,7 +176,7 @@ class UserModel : public Wt::WFormModel return false; } - std::optional _userId; + std::optional _userId; ::Auth::IPasswordService* _authPasswordService {}; }; @@ -196,7 +196,7 @@ UserView::refreshView() if (!wApp->internalPathMatches("/admin/user")) return; - const std::optional userId {StringUtils::readAs(wApp->internalPathNextPart("/admin/user/"))}; + const std::optional userId {StringUtils::readAs(wApp->internalPathNextPart("/admin/user/"))}; clear(); @@ -212,7 +212,7 @@ UserView::refreshView() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *userId)}; + const User::pointer user {User::find(LmsApp->getDbSession(), *userId)}; if (!user) throw UserNotFoundException {}; diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index ae0279a9..a5daed2f 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -33,6 +33,8 @@ namespace UserInterface { +using namespace Database; + UsersView::UsersView() : Wt::WTemplate {Wt::WString::tr("Lms.Admin.Users.template")} { @@ -69,9 +71,9 @@ UsersView::refreshView() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - for (const auto& user : Database::User::getAll(LmsApp->getDbSession())) + for (const UserId userId : User::find(LmsApp->getDbSession(), Range {}).results) { - const Database::UserId userId {user->getId()}; + const User::pointer user {User::find(LmsApp->getDbSession(), userId)}; Wt::WTemplate* entry {_container->addNew(Wt::WString::tr("Lms.Admin.Users.template.entry"))}; @@ -109,7 +111,7 @@ UsersView::refreshView() { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)}; + User::pointer user {User::find(LmsApp->getDbSession(), userId)}; if (user) user.remove(); diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 0d6d3619..f1df91a8 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -31,104 +31,92 @@ namespace UserInterface { using namespace Database; - std::vector> - ArtistCollector::get(std::optional range, bool& moreResults) + RangeResults + ArtistCollector::get(Database::Range range) { Scrobbling::IScrobblingService& scrobbling {*Service::get()}; range = getActualRange(range); - std::vector artists; + RangeResults artists; switch (getMode()) { case Mode::Random: - artists = getRandomArtists(range, moreResults); + artists = getRandomArtists(range); break; case Mode::Starred: - artists = Artist::getStarred(LmsApp->getDbSession(), - LmsApp->getUser(), - getFilters().getClusterIds(), - _linkType, - Artist::SortMethod::BySortName, - range, moreResults); + artists = scrobbling.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); break; case Mode::RecentlyPlayed: - for (const ArtistId artistId : scrobbling.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range, moreResults)) - { - if (Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), artistId)}) - artists.push_back(artist); - } + artists = scrobbling.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); break; case Mode::MostPlayed: - for (const ArtistId artistId : scrobbling.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range, moreResults)) - { - if (Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), artistId)}) - artists.push_back(artist); - } + artists = scrobbling.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); break; case Mode::RecentlyAdded: - artists = Artist::getLastWritten(LmsApp->getDbSession(), - std::nullopt, // after - getFilters().getClusterIds(), - _linkType, - range, moreResults); - break; - - case Mode::Search: - artists = Database::Artist::getByFilter(LmsApp->getDbSession(), - getFilters().getClusterIds(), - getSearchKeywords(), - std::nullopt, // no link - Database::Artist::SortMethod::BySortName, - range, moreResults); - break; - - case Mode::All: - artists = Artist::getByFilter(LmsApp->getDbSession(), - getFilters().getClusterIds(), - {}, - _linkType, - Artist::SortMethod::BySortName, - range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return artists; - } - - std::vector - ArtistCollector::getRandomArtists(std::optional range, bool& moreResults) - { - std::vector artists; - - assert(getMode() == Mode::Random); - - if (_randomArtists.empty()) - _randomArtists = Artist::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), _linkType, getMaxCount()); - - { - auto itBegin {std::cbegin(_randomArtists) + std::min(range ? range->offset : 0, _randomArtists.size())}; - auto itEnd {std::cbegin(_randomArtists) + std::min(range ? range->offset + range->limit : _randomArtists.size(), _randomArtists.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Artist::pointer artist {Artist::getById(LmsApp->getDbSession(), *it)}; - if (artist) - artists.push_back(artist); + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::LastWritten); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomArtists)); + case Mode::Search: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); + + artists = Artist::find(LmsApp->getDbSession(), params); + break; + } } + if (range.offset + range.size == getMaxCount()) + artists.moreResults = false; + return artists; } + RangeResults + ArtistCollector::getRandomArtists(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomArtists) + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::Random); + params.setRange(Range {0, getMaxCount()}); + + _randomArtists = Artist::find(LmsApp->getDbSession(), params); + } + + return _randomArtists->getSubRange(range); + } } // ns UserInterface diff --git a/src/lms/ui/explore/ArtistCollector.hpp b/src/lms/ui/explore/ArtistCollector.hpp index f529b4cc..ab9d43fc 100644 --- a/src/lms/ui/explore/ArtistCollector.hpp +++ b/src/lms/ui/explore/ArtistCollector.hpp @@ -20,9 +20,9 @@ #pragma once #include -#include #include "DatabaseCollectorBase.hpp" +#include "services/database/ArtistId.hpp" #include "services/database/Types.hpp" namespace Database @@ -37,13 +37,13 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - void reset() { _randomArtists.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomArtists.reset(); } void setArtistLinkType(std::optional linkType) { _linkType = linkType; } private: - std::vector> getRandomArtists(std::optional range, bool& moreResults); - std::vector _randomArtists; + Database::RangeResults getRandomArtists(Range range); + std::optional> _randomArtists; std::optional _linkType; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ArtistListHelpers.hpp b/src/lms/ui/explore/ArtistListHelpers.hpp index 412ad51f..d26a5182 100644 --- a/src/lms/ui/explore/ArtistListHelpers.hpp +++ b/src/lms/ui/explore/ArtistListHelpers.hpp @@ -23,7 +23,7 @@ #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" namespace Database { diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index e4c65a01..2c14fead 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -32,7 +32,7 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/User.hpp" -#include "services/feedback/IFeedbackService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -79,14 +79,14 @@ extractArtistIdFromInternalPath() if (mbid) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (const Database::Artist::pointer artist {Database::Artist::getByMBID(LmsApp->getDbSession(), *mbid)}) + if (const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), *mbid)}) return artist->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); + return StringUtils::readAs(wApp->internalPathNextPart("/artist/")); } void @@ -107,7 +107,7 @@ Artist::refreshView() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), *artistId)}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), *artistId)}; if (!artist) throw ArtistNotFoundException {}; @@ -166,14 +166,14 @@ Artist::refreshView() artistsAction.emit(PlayQueueAction::PlayLast, {_artistId}); }); - const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), _artistId)}; + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), _artistId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(this, [=] { if (isStarred) - Service::get()->unstar(LmsApp->getUserId(), _artistId); + Service::get()->unstar(LmsApp->getUserId(), _artistId); else - Service::get()->star(LmsApp->getUserId(), _artistId); + Service::get()->star(LmsApp->getUserId(), _artistId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(*artistId)}); @@ -184,7 +184,7 @@ Artist::refreshView() } void -Artist::refreshReleases(const Database::ObjectPtr& artist) +Artist::refreshReleases(const ObjectPtr& artist) { const auto releases {artist->getReleases(_filters->getClusterIds())}; if (releases.empty()) @@ -200,7 +200,7 @@ Artist::refreshReleases(const Database::ObjectPtr& artist) } void -Artist::refreshNonReleaseTracks(const Database::ObjectPtr& artist) +Artist::refreshNonReleaseTracks(const ObjectPtr& artist) { if (!artist->hasNonReleaseTracks()) return; @@ -216,7 +216,7 @@ Artist::refreshNonReleaseTracks(const Database::ObjectPtr& art } void -Artist::refreshSimilarArtists(const std::vector& similarArtistsId) +Artist::refreshSimilarArtists(const std::vector& similarArtistsId) { if (similarArtistsId.empty()) return; @@ -224,9 +224,9 @@ Artist::refreshSimilarArtists(const std::vector& similarArti setCondition("if-has-similar-artists", true); Wt::WContainerWidget* similarArtistsContainer {bindNew("similar-artists")}; - for (const Database::ArtistId artistId : similarArtistsId) + for (const ArtistId artistId : similarArtistsId) { - const Database::Artist::pointer similarArtist{Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + const Database::Artist::pointer similarArtist {Database::Artist::find(LmsApp->getDbSession(), artistId)}; if (!similarArtist) continue; @@ -252,27 +252,24 @@ Artist::refreshLinks(const Database::Artist::pointer& artist) void Artist::addSomeNonReleaseTracks() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; + if (!artist) + return; + + const auto tracks {artist->getNonReleaseTracks(std::nullopt, Range {static_cast(_trackContainer->getCount()), _tracksBatchSize})}; + bool moreResults {tracks.moreResults}; + + for (const Track::pointer& track : tracks.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; - if (!artist) - return; - - const auto tracks {artist->getNonReleaseTracks(std::nullopt, Database::Range {static_cast(_trackContainer->getCount()), _tracksBatchSize}, moreResults)}; - - for (const auto& track : tracks) + if (_trackContainer->getCount() == _tracksMaxCount) { - if (_trackContainer->getCount() == _tracksMaxCount) - { - moreResults = false; - break; - } - - _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); + moreResults = false; + break; } + + _trackContainer->add(TrackListHelpers::createEntry(track, tracksAction)); } _trackContainer->setHasMore(moreResults); diff --git a/src/lms/ui/explore/ArtistView.hpp b/src/lms/ui/explore/ArtistView.hpp index 3d292760..26255a6a 100644 --- a/src/lms/ui/explore/ArtistView.hpp +++ b/src/lms/ui/explore/ArtistView.hpp @@ -25,7 +25,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database diff --git a/src/lms/ui/explore/ArtistsView.cpp b/src/lms/ui/explore/ArtistsView.cpp index b804f453..34fc0966 100644 --- a/src/lms/ui/explore/ArtistsView.cpp +++ b/src/lms/ui/explore/ArtistsView.cpp @@ -21,6 +21,7 @@ #include +#include "services/database/Artist.hpp" #include "services/database/Session.hpp" #include "services/database/TrackArtistLink.hpp" #include "utils/EnumSet.hpp" @@ -109,7 +110,7 @@ Artists::refreshView(ArtistCollector::Mode mode) } void -Artists::refreshView(std::optional linkType) +Artists::refreshView(std::optional linkType) { _artistCollector.setArtistLinkType(linkType); refreshView(); @@ -120,13 +121,13 @@ Artists::refreshArtistLinkTypes() { std::shared_ptr linkTypeModel {std::static_pointer_cast(_linkType->model())}; - EnumSet usedLinkTypes; + EnumSet usedLinkTypes; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - usedLinkTypes = Database::TrackArtistLink::getUsedTypes(LmsApp->getDbSession()); + usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession()); } - auto addTypeIfUsed {[&](Database::TrackArtistLinkType linkType, std::string_view stringKey) + auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey) { if (!usedLinkTypes.contains(linkType)) return; @@ -151,17 +152,16 @@ Artists::refreshArtistLinkTypes() void Artists::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto artistIds {_artistCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + for (const ArtistId artistId : artistIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto artists {_artistCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - for (const auto& artist : artists) - _container->add(ArtistListHelpers::createEntry(artist)); + const auto artist {Artist::find(LmsApp->getDbSession(), artistId)}; + _container->add(ArtistListHelpers::createEntry(artist)); } - _container->setHasMore(moreResults); + _container->setHasMore(artistIds.moreResults); } } // namespace UserInterface diff --git a/src/lms/ui/explore/DatabaseCollectorBase.cpp b/src/lms/ui/explore/DatabaseCollectorBase.cpp index a4e9cdfc..7a161fd7 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.cpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.cpp @@ -23,28 +23,23 @@ namespace UserInterface { - DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount) + DatabaseCollectorBase::DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::size_t maxCount) : _filters {filters} , _mode {defaultMode} , _maxCount {maxCount} { } - std::optional - DatabaseCollectorBase::getActualRange(std::optional range) const + DatabaseCollectorBase::Range + DatabaseCollectorBase::getActualRange(Range range) const { - if (std::optional maxCount {getMaxCount()}) - { - if (range) - range->limit = std::min(*maxCount - range->offset, range->limit); - else - range = Range {0, *maxCount}; - } + assert(range.offset < _maxCount); + range.size = std::min(_maxCount - range.offset, range.size); return range; } - std::optional + std::size_t DatabaseCollectorBase::getMaxCount() const { return _maxCount; diff --git a/src/lms/ui/explore/DatabaseCollectorBase.hpp b/src/lms/ui/explore/DatabaseCollectorBase.hpp index db38fe5b..66732c32 100644 --- a/src/lms/ui/explore/DatabaseCollectorBase.hpp +++ b/src/lms/ui/explore/DatabaseCollectorBase.hpp @@ -49,17 +49,16 @@ namespace UserInterface All }; - DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::optional maxCount = std::nullopt); + DatabaseCollectorBase(Filters& filters, Mode defaultMode, std::size_t maxCount); Mode getMode() const { return _mode; } void setMode(Mode mode) { _mode = mode; } - void setMaxCount(std::size_t maxCount) { _maxCount = maxCount; } void setSearch(std::string_view search); protected: - std::optional getActualRange(std::optional range) const; - std::optional getMaxCount() const; - Filters& getFilters() { return _filters; } + Range getActualRange(Range range) const; + std::size_t getMaxCount() const; + Filters& getFilters() { return _filters; } const std::vector& getSearchKeywords() const { return _searchKeywords; } private: @@ -67,7 +66,7 @@ namespace UserInterface std::string _searchText; std::vector _searchKeywords; Mode _mode; - std::optional _maxCount; + std::size_t _maxCount; }; } // ns UserInterface diff --git a/src/lms/ui/explore/Explore.cpp b/src/lms/ui/explore/Explore.cpp index b0ab61a7..bf9b1239 100644 --- a/src/lms/ui/explore/Explore.cpp +++ b/src/lms/ui/explore/Explore.cpp @@ -138,7 +138,7 @@ getArtistsTracks(Database::Session& session, const std::vectorgetDbSession().createSharedTransaction()}; - const auto types {Database::ClusterType::getAllUsed(LmsApp->getDbSession())}; - for (const Database::ClusterType::pointer& type : types) - typeCombo->addItem(Wt::WString::fromUTF8(type->getName())); - - if (!types.empty()) + const auto clusterTypesIds {ClusterType::findUsed(LmsApp->getDbSession(), Range {})}; + for (const ClusterTypeId clusterTypeId : clusterTypesIds.results) { - for (const Database::Cluster::pointer& cluster : types.front()->getClusters()) + const auto clusterType {ClusterType::find(LmsApp->getDbSession(), clusterTypeId)}; + typeCombo->addItem(Wt::WString::fromUTF8(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(cluster->getName())); @@ -74,9 +81,9 @@ Filters::showDialog() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - auto clusterType = Database::ClusterType::getByName(LmsApp->getDbSession(), name); + auto clusterType = ClusterType::find(LmsApp->getDbSession(), name); - for (const Database::Cluster::pointer& cluster : clusterType->getClusters()) + 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(cluster->getName())); @@ -98,16 +105,16 @@ Filters::showDialog() const std::string value {valueCombo->valueText().toUTF8()}; // TODO use a model to store the cluster.id() values - Database::ClusterId clusterId {}; + ClusterId clusterId {}; { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)}; + ClusterType::pointer clusterType {ClusterType::find(LmsApp->getDbSession(), type)}; if (!clusterType) return; - Database::Cluster::pointer cluster {clusterType->getCluster(value)}; + Cluster::pointer cluster {clusterType->getCluster(value)}; if (!cluster) return; @@ -121,7 +128,7 @@ Filters::showDialog() } void -Filters::add(Database::ClusterId clusterId) +Filters::add(ClusterId clusterId) { Wt::WInteractWidget* filter {}; @@ -129,7 +136,7 @@ Filters::add(Database::ClusterId clusterId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)}; + Cluster::pointer cluster {Cluster::find(LmsApp->getDbSession(), clusterId)}; if (!cluster) return; @@ -144,7 +151,7 @@ Filters::add(Database::ClusterId clusterId) filter->clicked().connect([=] { _filters->removeWidget(filter); - _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](Database::ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); + _clusterIds.erase(std::remove_if(std::begin(_clusterIds), std::end(_clusterIds), [clusterId](ClusterId id) { return id == clusterId; }), std::end(_clusterIds)); _sigUpdated.emit(); }); diff --git a/src/lms/ui/explore/Filters.hpp b/src/lms/ui/explore/Filters.hpp index 585d1472..0f96398a 100644 --- a/src/lms/ui/explore/Filters.hpp +++ b/src/lms/ui/explore/Filters.hpp @@ -24,7 +24,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/ClusterId.hpp" #include "Filters.hpp" diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index e0d0efb5..e109f038 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -34,100 +34,89 @@ namespace UserInterface { using namespace Database; - std::vector - ReleaseCollector::get(std::optional range, bool& moreResults) + RangeResults + ReleaseCollector::get(Database::Range range) { Scrobbling::IScrobblingService& scrobbling {*Service::get()}; range = getActualRange(range); - std::vector releases; + RangeResults releases; switch (getMode()) { case Mode::Random: - releases = getRandomReleases(range, moreResults); + releases = getRandomReleases(range); break; case Mode::Starred: - releases = Release::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + releases = scrobbling.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case ReleaseCollector::Mode::RecentlyPlayed: - for (const ReleaseId releaseId : scrobbling.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range, moreResults)) - { - if (const Release::pointer release {Release::getById(LmsApp->getDbSession(), releaseId)}) - releases.push_back(release); - } + releases = scrobbling.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::MostPlayed: - for (const ReleaseId releaseId : scrobbling.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range, moreResults)) - { - if (const Release::pointer release {Release::getById(LmsApp->getDbSession(), releaseId)}) - releases.push_back(release); - } + releases = scrobbling.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::RecentlyAdded: - releases = Release::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults); - break; - - case Mode::Search: - releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults); - break; - - case Mode::All: - releases = Release::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return releases; - } - - std::vector - ReleaseCollector::getAll() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - bool moreResults; - const auto releases {get(std::nullopt, moreResults)}; - - std::vector res; - res.reserve(releases.size()); - std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const Release::pointer& release) { return release->getId(); }); - - return res; - } - - std::vector - ReleaseCollector::getRandomReleases(std::optional range, bool& moreResults) - { - std::vector releases; - - assert(getMode() == Mode::Random); - - if (_randomReleases.empty()) - _randomReleases = Release::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount()); - - { - auto itBegin {std::cbegin(_randomReleases) + std::min(range ? range->offset : 0, _randomReleases.size())}; - auto itEnd {std::cbegin(_randomReleases) + std::min(range ? range->offset + range->limit : _randomReleases.size(), _randomReleases.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Release::pointer release {Release::getById(LmsApp->getDbSession(), *it)}; - if (release) - releases.push_back(release); + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomReleases)); + case Mode::Search: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); + + releases = Release::find(LmsApp->getDbSession(), params); + break; + } } + if (range.offset + range.size == getMaxCount()) + releases.moreResults = false; + return releases; } + RangeResults + ReleaseCollector::getRandomReleases(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomReleases) + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({0, getMaxCount()}); + _randomReleases = Release::find(LmsApp->getDbSession(), params); + } + + return _randomReleases->getSubRange(range); + } + } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseCollector.hpp b/src/lms/ui/explore/ReleaseCollector.hpp index 3882cc80..924aabe6 100644 --- a/src/lms/ui/explore/ReleaseCollector.hpp +++ b/src/lms/ui/explore/ReleaseCollector.hpp @@ -23,6 +23,10 @@ #include "DatabaseCollectorBase.hpp" +#include "services/database/Object.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/Types.hpp" + namespace Database { class Release; @@ -35,13 +39,12 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); - void reset() { _randomReleases.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomReleases.reset(); } private: - std::vector> getRandomReleases(std::optional range, bool& moreResults); - std::vector _randomReleases; + Database::RangeResults getRandomReleases(Range range); + std::optional> _randomReleases; }; } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseListHelpers.hpp b/src/lms/ui/explore/ReleaseListHelpers.hpp index fd3b051a..41550b85 100644 --- a/src/lms/ui/explore/ReleaseListHelpers.hpp +++ b/src/lms/ui/explore/ReleaseListHelpers.hpp @@ -22,7 +22,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" namespace Database { diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index cc970b0f..ae8aefcb 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -24,7 +24,7 @@ #include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "services/database/User.hpp" -#include "services/feedback/IFeedbackService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "resource/DownloadResource.hpp" #include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -49,14 +49,14 @@ namespace UserInterface releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); }); - const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), releaseId)}; + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), releaseId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(&target, [=] { if (isStarred) - Service::get()->unstar(LmsApp->getUserId(), releaseId); + Service::get()->unstar(LmsApp->getUserId(), releaseId); else - Service::get()->star(LmsApp->getUserId(), releaseId); + Service::get()->star(LmsApp->getUserId(), releaseId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(releaseId)}); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 199810dc..2e3f2deb 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -78,14 +78,14 @@ extractReleaseIdFromInternalPath() if (mbid) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - if (const Database::Release::pointer release {Database::Release::getByMBID(LmsApp->getDbSession(), *mbid)}) + if (const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *mbid)}) return release->getId(); } return std::nullopt; } - return StringUtils::readAs(wApp->internalPathNextPart("/release/")); + return StringUtils::readAs(wApp->internalPathNextPart("/release/")); } @@ -105,7 +105,7 @@ Release::refreshView() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), *releaseId)}; + const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), *releaseId)}; if (!release) throw ReleaseNotFoundException {}; @@ -218,7 +218,7 @@ Release::refreshView() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - const auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})}; + const auto artists {track->getArtists({TrackArtistLinkType::Artist})}; if (variousArtists && !artists.empty()) { entry->setCondition("if-has-artists", true); @@ -252,7 +252,7 @@ Release::refreshView() entry->bindString("duration", trackDurationToString(track->getDuration()), Wt::TextFormat::Plain); - LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (Database::TrackId loadedTrackId) + LmsApp->getMediaPlayer().trackLoaded.connect(entry, [=] (TrackId loadedTrackId) { entry->bindString("is-playing", loadedTrackId == trackId ? "Lms-entry-playing" : ""); }); @@ -270,12 +270,12 @@ Release::refreshView() void Release::refreshReleaseArtists(const Database::Release::pointer& release) { - std::vector> artists; + std::vector> artists; artists = release->getReleaseArtists(); if (artists.empty()) { - artists = release->getArtists(Database::TrackArtistLinkType::Artist); + artists = release->getArtists(TrackArtistLinkType::Artist); if (artists.size() > 1) { setCondition("if-has-various-release-artists", true); @@ -340,7 +340,7 @@ Release::refreshLinks(const Database::Release::pointer& release) } void -Release::refreshSimilarReleases(const std::vector& similarReleasesId) +Release::refreshSimilarReleases(const std::vector& similarReleasesId) { if (similarReleasesId.empty()) return; @@ -348,9 +348,9 @@ Release::refreshSimilarReleases(const std::vector& similarR setCondition("if-has-similar-releases", true); auto* similarReleasesContainer {bindNew("similar-releases")}; - for (const Database::ReleaseId id : similarReleasesId) + for (const ReleaseId id : similarReleasesId) { - const Database::Release::pointer similarRelease{Database::Release::getById(LmsApp->getDbSession(), id)}; + const Database::Release::pointer similarRelease {Database::Release::find(LmsApp->getDbSession(), id)}; if (!similarRelease) continue; diff --git a/src/lms/ui/explore/ReleaseView.hpp b/src/lms/ui/explore/ReleaseView.hpp index 855bc044..41f9fbf6 100644 --- a/src/lms/ui/explore/ReleaseView.hpp +++ b/src/lms/ui/explore/ReleaseView.hpp @@ -22,7 +22,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database diff --git a/src/lms/ui/explore/ReleasesView.cpp b/src/lms/ui/explore/ReleasesView.cpp index 8e47fe4e..6fc358da 100644 --- a/src/lms/ui/explore/ReleasesView.cpp +++ b/src/lms/ui/explore/ReleasesView.cpp @@ -23,6 +23,7 @@ #include #include +#include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "common/InfiniteScrollingContainer.hpp" #include "ReleaseListHelpers.hpp" @@ -115,23 +116,24 @@ Releases::refreshView(ReleaseCollector::Mode mode) void Releases::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto releaseIds {_releaseCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + for (const ReleaseId releaseId : releaseIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto releases {_releaseCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - for (const auto& release : releases) - _container->add(ReleaseListHelpers::createEntry(release)); + const Release::pointer release {Release::find(LmsApp->getDbSession(), releaseId)}; + _container->add(ReleaseListHelpers::createEntry(release)); } - _container->setHasMore(moreResults); + _container->setHasMore(releaseIds.moreResults); } -std::vector +std::vector Releases::getAllReleases() { - return _releaseCollector.getAll(); + RangeResults releaseIds {_releaseCollector.get(Range {})}; + + return std::move(releaseIds.results); } } // namespace UserInterface diff --git a/src/lms/ui/explore/SearchView.cpp b/src/lms/ui/explore/SearchView.cpp index b82ff3a0..ac573799 100644 --- a/src/lms/ui/explore/SearchView.cpp +++ b/src/lms/ui/explore/SearchView.cpp @@ -38,6 +38,8 @@ #include "ReleaseListHelpers.hpp" #include "TrackListHelpers.hpp" +using namespace Database; + namespace UserInterface { SearchView::SearchView(Filters* filters) @@ -133,17 +135,22 @@ namespace UserInterface SearchView::addSomeArtists() { InfiniteScrollingContainer& results {getResultContainer(Mode::Artist)}; - bool moreResults {}; { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Artist)}; - for (const auto& artist : _artistCollector.get(range, moreResults)) + const Range range {results.getCount(), getBatchSize(Mode::Artist)}; + const RangeResults artistIds {_artistCollector.get(range)}; + for (const ArtistId artistId : artistIds.results) + { + const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)}; results.add(ArtistListHelpers::createEntry(artist)); - } + } - results.setHasMore(moreResults); + results.setHasMore(artistIds.moreResults); + } getItemMenu(Mode::Artist).setDisabled(results.getCount() == 0); } @@ -155,14 +162,21 @@ namespace UserInterface bool moreResults {}; { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Release)}; - for (const auto& release : _releaseCollector.get(range, moreResults)) - results.add(ReleaseListHelpers::createEntry(release)); - } + const Range range {results.getCount(), getBatchSize(Mode::Release)}; + const RangeResults releaseIds {_releaseCollector.get(range)}; - results.setHasMore(moreResults); + for (const ReleaseId releaseId : releaseIds.results) + { + const Release::pointer release {Release::find(LmsApp->getDbSession(), releaseId)}; + results.add(ReleaseListHelpers::createEntry(release)); + } + + results.setHasMore(moreResults); + } getItemMenu(Mode::Release).setDisabled(results.getCount() == 0); } @@ -171,17 +185,22 @@ namespace UserInterface SearchView::addSomeTracks() { InfiniteScrollingContainer& results {getResultContainer(Mode::Track)}; - bool moreResults {}; - { + using namespace Database; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Range range {results.getCount(), getBatchSize(Mode::Track)}; - for (const auto& track : _trackCollector.get(range, moreResults)) - results.add(TrackListHelpers::createEntry(track, tracksAction)); - } + const Range range {results.getCount(), getBatchSize(Mode::Track)}; + const RangeResults trackIds {_trackCollector.get(range)}; - results.setHasMore(moreResults); + for (const TrackId trackId : trackIds.results) + { + const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; + results.add(TrackListHelpers::createEntry(track, tracksAction)); + } + + results.setHasMore(trackIds.moreResults); + } getItemMenu(Mode::Track).setDisabled(results.getCount() == 0); } diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index 13be14fc..bbefe4cd 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -34,99 +34,86 @@ namespace UserInterface { using namespace Database; - std::vector - TrackCollector::get(std::optional range, bool& moreResults) + RangeResults + TrackCollector::get(Range range) { Scrobbling::IScrobblingService& scrobbling {*Service::get()}; range = getActualRange(range); - std::vector tracks; + RangeResults tracks; switch (getMode()) { case Mode::Random: - tracks = getRandomTracks(range, moreResults); + tracks = getRandomTracks(range); break; case Mode::Starred: - tracks = Track::getStarred(LmsApp->getDbSession(), LmsApp->getUser(), getFilters().getClusterIds(), range, moreResults); + tracks = scrobbling.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case TrackCollector::Mode::RecentlyPlayed: - for (const TrackId trackId : scrobbling.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range, moreResults)) - { - if (const Track::pointer track {Track::getById(LmsApp->getDbSession(), trackId)}) - tracks.push_back(track); - } + tracks = scrobbling.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::MostPlayed: - for (const TrackId trackId : scrobbling.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range, moreResults)) - { - if (const Track::pointer track {Track::getById(LmsApp->getDbSession(), trackId)}) - tracks.push_back(track); - } + tracks = scrobbling.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::RecentlyAdded: - tracks = Track::getLastWritten(LmsApp->getDbSession(), std::nullopt, getFilters().getClusterIds(), range, moreResults); - break; - - case Mode::Search: - tracks = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), getSearchKeywords(), range, moreResults); - break; - - case Mode::All: - tracks = Track::getByFilter(LmsApp->getDbSession(), getFilters().getClusterIds(), {}, range, moreResults); - break; - } - - if (range && getMaxCount() && (range->offset + range->limit == *getMaxCount())) - moreResults = false; - - return tracks; - } - - std::vector - TrackCollector::getAll() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - bool moreResults; - const auto tracks {get(std::nullopt, moreResults)}; - - std::vector res; - res.reserve(tracks.size()); - std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const Track::pointer& track) { return track->getId(); }); - - return res; - } - - std::vector - TrackCollector::getRandomTracks(std::optional range, bool& moreResults) - { - std::vector tracks; - - assert(getMode() == Mode::Random); - - if (_randomTracks.empty()) - _randomTracks = Track::getAllIdsRandom(LmsApp->getDbSession(), getFilters().getClusterIds(), getMaxCount()); - - { - auto itBegin {std::cbegin(_randomTracks) + std::min(range ? range->offset : 0, _randomTracks.size())}; - auto itEnd {std::cbegin(_randomTracks) + std::min(range ? range->offset + range->limit : _randomTracks.size(), _randomTracks.size())}; - - for (auto it {itBegin}; it != itEnd; ++it) { - Track::pointer track {Track::getById(LmsApp->getDbSession(), *it)}; - if (track) - tracks.push_back(track); + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(TrackSortMethod::LastWritten); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; } - moreResults = (itEnd != std::cend(_randomTracks)); + case Mode::Search: + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; + } + + case Mode::All: + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setRange(range); + + tracks = Track::find(LmsApp->getDbSession(), params); + break; + } } + if (range.offset + range.size == getMaxCount()) + tracks.moreResults = false; + return tracks; } + RangeResults + TrackCollector::getRandomTracks(Range range) + { + assert(getMode() == Mode::Random); + + if (!_randomTracks) + { + Track::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(TrackSortMethod::Random); + params.setRange({0, getMaxCount()}); + _randomTracks = Track::find(LmsApp->getDbSession(), params); + } + + return _randomTracks->getSubRange(range); + } + } // ns UserInterface diff --git a/src/lms/ui/explore/TrackCollector.hpp b/src/lms/ui/explore/TrackCollector.hpp index 79011adf..a87f9759 100644 --- a/src/lms/ui/explore/TrackCollector.hpp +++ b/src/lms/ui/explore/TrackCollector.hpp @@ -21,6 +21,8 @@ #include +#include "services/database/Object.hpp" +#include "services/database/TrackId.hpp" #include "DatabaseCollectorBase.hpp" namespace Database @@ -35,13 +37,12 @@ namespace UserInterface public: using DatabaseCollectorBase::DatabaseCollectorBase; - std::vector> get(std::optional range, bool& moreResults); - std::vector getAll(); - void reset() { _randomTracks.clear(); } + Database::RangeResults get(Database::Range range); + void reset() { _randomTracks.reset(); } private: - std::vector> getRandomTracks(std::optional range, bool& moreResults); - std::vector _randomTracks; + Database::RangeResults getRandomTracks(Range range); + std::optional> _randomTracks; }; } // ns UserInterface diff --git a/src/lms/ui/explore/TrackListHelpers.hpp b/src/lms/ui/explore/TrackListHelpers.hpp index 2ba93246..00b322f9 100644 --- a/src/lms/ui/explore/TrackListHelpers.hpp +++ b/src/lms/ui/explore/TrackListHelpers.hpp @@ -22,7 +22,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/Object.hpp" #include "PlayQueueAction.hpp" namespace Database diff --git a/src/lms/ui/explore/TrackPopup.cpp b/src/lms/ui/explore/TrackPopup.cpp index f95e5825..7020d2d4 100644 --- a/src/lms/ui/explore/TrackPopup.cpp +++ b/src/lms/ui/explore/TrackPopup.cpp @@ -24,7 +24,7 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/User.hpp" -#include "services/feedback/IFeedbackService.hpp" +#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "resource/DownloadResource.hpp" #include "LmsApplication.hpp" @@ -44,20 +44,20 @@ namespace UserInterface tracksAction.emit(PlayQueueAction::PlayLast, {trackId}); }); - bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; + const bool isStarred {Service::get()->isStarred(LmsApp->getUserId(), trackId)}; popup->addItem(Wt::WString::tr(isStarred ? "Lms.Explore.unstar" : "Lms.Explore.star")) ->triggered().connect(&target, [=] { auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - auto track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + auto track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) return; if (isStarred) - Service::get()->unstar(LmsApp->getUserId(), trackId); + Service::get()->unstar(LmsApp->getUserId(), trackId); else - Service::get()->star(LmsApp->getUserId(), trackId); + Service::get()->star(LmsApp->getUserId(), trackId); }); popup->addItem(Wt::WString::tr("Lms.Explore.download")) ->setLink(Wt::WLink {std::make_unique(trackId)}); diff --git a/src/lms/ui/explore/TracksView.cpp b/src/lms/ui/explore/TracksView.cpp index 639046de..c93e9a18 100644 --- a/src/lms/ui/explore/TracksView.cpp +++ b/src/lms/ui/explore/TracksView.cpp @@ -24,6 +24,7 @@ #include #include "services/database/Session.hpp" +#include "services/database/Track.hpp" #include "utils/Logger.hpp" #include "common/InfiniteScrollingContainer.hpp" @@ -118,24 +119,25 @@ Tracks::refreshView(TrackCollector::Mode mode) void Tracks::addSome() { - bool moreResults {}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + const auto trackIds {_trackCollector.get(Range {static_cast(_container->getCount()), _batchSize})}; + + for (const TrackId trackId : trackIds.results) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - const auto tracks {_trackCollector.get(Range {static_cast(_container->getCount()), _batchSize}, moreResults)}; - - for (const auto& track : tracks) - _container->add(TrackListHelpers::createEntry(track, tracksAction)); + const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)}; + _container->add(TrackListHelpers::createEntry(track, tracksAction)); } - _container->setHasMore(moreResults); + _container->setHasMore(trackIds.moreResults); } std::vector Tracks::getAllTracks() { - return _trackCollector.getAll(); + RangeResults trackIds {_trackCollector.get(Range {})}; + + return std::move(trackIds.results); } diff --git a/src/lms/ui/resource/AudioFileResource.cpp b/src/lms/ui/resource/AudioFileResource.cpp index 42ff45b6..792128c0 100644 --- a/src/lms/ui/resource/AudioFileResource.cpp +++ b/src/lms/ui/resource/AudioFileResource.cpp @@ -51,7 +51,7 @@ getTrackPathFromTrackId(Database::TrackId trackId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (!track) { LOG(ERROR) << "Missing track"; diff --git a/src/lms/ui/resource/AudioFileResource.hpp b/src/lms/ui/resource/AudioFileResource.hpp index 046f7c55..589ac51e 100644 --- a/src/lms/ui/resource/AudioFileResource.hpp +++ b/src/lms/ui/resource/AudioFileResource.hpp @@ -21,7 +21,7 @@ #include -#include "services/database/Types.hpp" +#include "services/database/TrackId.hpp" namespace UserInterface { diff --git a/src/lms/ui/resource/AudioTranscodeResource.cpp b/src/lms/ui/resource/AudioTranscodeResource.cpp index acc04918..c8982fbd 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.cpp +++ b/src/lms/ui/resource/AudioTranscodeResource.cpp @@ -149,7 +149,7 @@ readTranscodeParameters(const Wt::Http::Request& request) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), *trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), *trackId)}; if (!track) { LOG(ERROR) << "Missing track"; @@ -158,7 +158,7 @@ readTranscodeParameters(const Wt::Http::Request& request) parameters.file = track->getPath(); - if (Database::User::audioTranscodeAllowedBitrates.find(*bitrate) == std::cend(Database::User::audioTranscodeAllowedBitrates)) + if (!Database::isAudioBitrateAllowed(*bitrate)) { LOG(ERROR) << "Bitrate '" << *bitrate << "' is not allowed"; return std::nullopt; diff --git a/src/lms/ui/resource/AudioTranscodeResource.hpp b/src/lms/ui/resource/AudioTranscodeResource.hpp index dbd1d31a..a4240b15 100644 --- a/src/lms/ui/resource/AudioTranscodeResource.hpp +++ b/src/lms/ui/resource/AudioTranscodeResource.hpp @@ -22,7 +22,7 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/TrackId.hpp" namespace Database { diff --git a/src/lms/ui/resource/CoverResource.hpp b/src/lms/ui/resource/CoverResource.hpp index 7ab2a1fe..533a96f1 100644 --- a/src/lms/ui/resource/CoverResource.hpp +++ b/src/lms/ui/resource/CoverResource.hpp @@ -20,7 +20,8 @@ #pragma once #include -#include "services/database/Types.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" namespace UserInterface { diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 7dd75d79..f87d4645 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -176,7 +176,7 @@ DownloadArtistResource::DownloadArtistResource(Database::ArtistId artistId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), artistId)}; if (artist) suggestFileName(getArtistPathName(artist) + ".zip"); } @@ -186,7 +186,7 @@ DownloadArtistResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; + const Database::Artist::pointer artist {Database::Artist::find(LmsApp->getDbSession(), _artistId)}; if (!artist) { LOG(DEBUG) << "Cannot find artist"; @@ -201,7 +201,7 @@ DownloadReleaseResource::DownloadReleaseResource(Database::ReleaseId releaseId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}; + Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)}; if (release) suggestFileName(getReleasePathName(release) + ".zip"); } @@ -212,7 +212,7 @@ DownloadReleaseResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)}; + const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), _releaseId)}; if (!release) { LOG(DEBUG) << "Cannot find release"; @@ -227,7 +227,7 @@ DownloadTrackResource::DownloadTrackResource(Database::TrackId trackId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), trackId)}; + Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), trackId)}; if (track) suggestFileName(getTrackPathName(track) + ".zip"); } @@ -237,7 +237,7 @@ DownloadTrackResource::createZipper() { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - const Database::Track::pointer track {Database::Track::getById(LmsApp->getDbSession(), _trackId)}; + const Database::Track::pointer track {Database::Track::find(LmsApp->getDbSession(), _trackId)}; if (!track) { LOG(DEBUG) << "Cannot find track"; diff --git a/src/lms/ui/resource/DownloadResource.hpp b/src/lms/ui/resource/DownloadResource.hpp index 80d02994..9203d0af 100644 --- a/src/lms/ui/resource/DownloadResource.hpp +++ b/src/lms/ui/resource/DownloadResource.hpp @@ -22,7 +22,9 @@ #include #include -#include "services/database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" #include "utils/Zipper.hpp" namespace UserInterface diff --git a/src/tools/cover/LmsCover.cpp b/src/tools/cover/LmsCover.cpp index 7d72fd09..36291e5b 100644 --- a/src/tools/cover/LmsCover.cpp +++ b/src/tools/cover/LmsCover.cpp @@ -38,13 +38,15 @@ static void dumpTrackCovers(Database::Session& session, Image::ImageSize width) { - std::vector trackIds; + using namespace Database; + + RangeResults trackIds; { auto transaction {session.createSharedTransaction()}; - trackIds = Database::Track::getAllIds(session); + trackIds = Database::Track::find(session, Database::Track::FindParameters {}); } - for (const Database::TrackId trackId : trackIds) + for (const Database::TrackId trackId : trackIds.results) { std::cout << "Getting cover for track id " << trackId.toString() << std::endl; Service::get()->getFromTrack(trackId, width); diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index 98666179..5dec1cb3 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -30,34 +30,37 @@ #include "services/database/Release.hpp" #include "services/database/Session.hpp" #include "services/database/Track.hpp" +#include "services/database/Types.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "utils/IConfig.hpp" #include "utils/Service.hpp" #include "utils/StreamLogger.hpp" +using namespace Database; + static void -dumpTracksRecommendation(Database::Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) +dumpTracksRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector trackIds {[&]() + const RangeResults trackIds {[&]() { auto transaction {session.createSharedTransaction()}; - return Database::Track::getAllIds(session); + return Track::find(session, Track::FindParameters {}); }()}; - std::cout << "*** Tracks (" << trackIds.size() << ") ***" << std::endl; - for (Database::TrackId trackId : trackIds) + std::cout << "*** Tracks (" << trackIds.results.size() << ") ***" << std::endl; + for (const TrackId trackId : trackIds.results) { - auto trackToString = [&](Database::TrackId trackId) + auto trackToString = [&](const TrackId trackId) { std::string res; auto transaction {session.createSharedTransaction()}; - Database::Track::pointer track {Database::Track::getById(session, trackId)}; + const Track::pointer track {Track::find(session, trackId)}; res += track->getName(); if (track->getRelease()) res += " [" + track->getRelease()->getName() + "]"; - for (auto artist : track->getArtists({Database::TrackArtistLinkType::Artist})) + for (auto artist : track->getArtists({TrackArtistLinkType::Artist})) res += " - " + artist->getName(); for (auto cluster : track->getClusters()) res += " {" + cluster->getType()->getName() + "-"+ cluster->getName() + "}"; @@ -66,61 +69,61 @@ dumpTracksRecommendation(Database::Session session, Recommendation::IRecommendat }; std::cout << "Processing track '" << trackToString(trackId) << std::endl; - for (Database::TrackId similarTrackId : recommendationService.getSimilarTracks({trackId}, maxSimilarityCount)) + for (TrackId similarTrackId : recommendationService.findSimilarTracks({trackId}, maxSimilarityCount)) std::cout << "\t- Similar track '" << trackToString(similarTrackId) << std::endl; } } static void -dumpReleasesRecommendation(Database::Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) +dumpReleasesRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector releaseIds = std::invoke([&]() + const RangeResults releaseIds {std::invoke([&]() { auto transaction {session.createSharedTransaction()}; - return Database::Release::getAllIds(session); - }); + return Release::find(session, Release::FindParameters {}); + })}; std::cout << "*** Releases ***" << std::endl; - for (Database::ReleaseId releaseId : releaseIds) + for (ReleaseId releaseId : releaseIds.results) { - auto releaseToString = [&](Database::ReleaseId releaseId) -> std::string + auto releaseToString = [&](ReleaseId releaseId) -> std::string { auto transaction {session.createSharedTransaction()}; - Database::Release::pointer release {Database::Release::getById(session, releaseId)}; + Release::pointer release {Release::find(session, releaseId)}; return release->getName(); }; std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl; - for (Database::ReleaseId similarReleaseId : recommendationService.getSimilarReleases(releaseId, maxSimilarityCount)) + for (ReleaseId similarReleaseId : recommendationService.getSimilarReleases(releaseId, maxSimilarityCount)) std::cout << "\t- Similar release '" << releaseToString(similarReleaseId) << "'" << std::endl; } } static void -dumpArtistsRecommendation(Database::Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) +dumpArtistsRecommendation(Session session, Recommendation::IRecommendationService& recommendationService, unsigned maxSimilarityCount) { - const std::vector artistIds = std::invoke([&]() + const RangeResults artistIds = std::invoke([&]() { auto transaction {session.createSharedTransaction()}; - return Database::Artist::getAllIds(session); + return Artist::find(session, Artist::FindParameters {}); }); std::cout << "*** Artists ***" << std::endl; - for (Database::ArtistId artistId : artistIds) + for (ArtistId artistId : artistIds.results) { - auto artistToString = [&](Database::ArtistId artistId) + auto artistToString = [&](ArtistId artistId) { auto transaction {session.createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(session, artistId)}; + Artist::pointer artist {Artist::find(session, artistId)}; return artist->getName(); }; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; - for (Database::ArtistId similarArtistId : recommendationService.getSimilarArtists(artistId, {Database::TrackArtistLinkType::Artist, Database::TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) + for (ArtistId similarArtistId : recommendationService.getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, maxSimilarityCount)) { std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; } @@ -158,8 +161,8 @@ int main(int argc, char *argv[]) Service config {createConfig(vm["conf"].as())}; - Database::Db db {config->getPath("working-dir") / "lms.db"}; - Database::Session session {db}; + Db db {config->getPath("working-dir") / "lms.db"}; + Session session {db}; std::cout << "Creating recommendation recommendationService..." << std::endl; const auto recommendationService {Recommendation::createRecommendationService(db)};