From 069470194b3cd7f793ef344216923b87094d256c Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 31 Oct 2023 13:37:14 +0100 Subject: [PATCH] Separated feedback services (stars / love for LB) from scrobbling services, to ease last.fm integration --- approot/messages.xml | 9 +- approot/messages_fr.xml | 9 +- approot/messages_it.xml | 9 +- approot/messages_zh.xml | 7 +- approot/settings.xml | 25 +- src/libs/services/CMakeLists.txt | 1 + src/libs/services/database/impl/Artist.cpp | 6 +- src/libs/services/database/impl/Listen.cpp | 64 +- src/libs/services/database/impl/Migration.cpp | 463 +------- src/libs/services/database/impl/Migration.hpp | 2 +- src/libs/services/database/impl/Release.cpp | 6 +- src/libs/services/database/impl/Session.cpp | 18 +- .../services/database/impl/StarredArtist.cpp | 71 +- .../services/database/impl/StarredRelease.cpp | 71 +- .../services/database/impl/StarredTrack.cpp | 96 +- src/libs/services/database/impl/Track.cpp | 6 +- src/libs/services/database/impl/User.cpp | 6 +- .../include/services/database/Artist.hpp | 4 +- .../include/services/database/Listen.hpp | 44 +- .../include/services/database/Release.hpp | 6 +- .../services/database/StarredArtist.hpp | 78 +- .../services/database/StarredRelease.hpp | 78 +- .../services/database/StarredTrack.hpp | 100 +- .../include/services/database/Track.hpp | 36 +- .../include/services/database/Types.hpp | 10 +- .../include/services/database/User.hpp | 35 +- src/libs/services/database/test/Listen.cpp | 248 ++-- .../services/database/test/StarredArtist.cpp | 136 ++- .../services/database/test/StarredRelease.cpp | 136 ++- .../services/database/test/StarredTrack.cpp | 136 ++- src/libs/services/feedback/CMakeLists.txt | 29 + .../feedback/impl/FeedbackService.cpp | 185 +++ .../feedback/impl/FeedbackService.hpp | 78 ++ .../feedback/impl/FeedbackService.impl.hpp | 113 ++ .../impl/IFeedbackBackend.hpp} | 31 +- .../impl/internal/InternalBackend.cpp | 84 ++ .../impl/internal/InternalBackend.hpp} | 15 +- .../feedback/impl/listenbrainz/Exception.hpp | 31 + .../impl/listenbrainz/FeedbackTypes.cpp | 4 +- .../impl/listenbrainz/FeedbackTypes.hpp | 4 +- .../impl/listenbrainz/FeedbacksParser.cpp | 90 ++ .../impl/listenbrainz/FeedbacksParser.hpp | 4 +- .../listenbrainz/FeedbacksSynchronizer.cpp | 490 ++++++++ .../listenbrainz/FeedbacksSynchronizer.hpp | 102 ++ .../impl/listenbrainz/ListenBrainzBackend.cpp | 103 ++ .../impl/listenbrainz/ListenBrainzBackend.hpp | 60 + .../feedback/impl/listenbrainz/Utils.cpp | 62 + .../feedback/impl/listenbrainz/Utils.hpp | 37 + .../include/services/feedback/Exception.hpp | 31 + .../services/feedback/IFeedbackService.hpp | 73 ++ src/libs/services/scrobbling/CMakeLists.txt | 7 +- .../scrobbling/impl/IScrobblingBackend.hpp | 48 + .../scrobbling/impl/ScrobblingService.cpp | 198 +--- .../scrobbling/impl/ScrobblingService.hpp | 67 +- .../impl/ScrobblingService.impl.hpp | 114 -- .../impl/internal/InternalBackend.cpp | 68 ++ .../impl/internal/InternalBackend.hpp | 44 + .../impl/internal/InternalScrobbler.cpp | 122 -- .../impl/listenbrainz/FeedbacksParser.cpp | 93 -- .../listenbrainz/FeedbacksSynchronizer.cpp | 505 -------- .../listenbrainz/FeedbacksSynchronizer.hpp | 106 -- .../impl/listenbrainz/ListenBrainzBackend.cpp | 87 ++ .../impl/listenbrainz/ListenBrainzBackend.hpp | 60 + .../listenbrainz/ListenBrainzScrobbler.cpp | 151 --- .../listenbrainz/ListenBrainzScrobbler.hpp | 72 -- .../impl/listenbrainz/ListensSynchronizer.cpp | 27 +- .../impl/listenbrainz/ListensSynchronizer.hpp | 2 +- .../scrobbling/impl/listenbrainz/Utils.hpp | 6 +- .../scrobbling/IScrobblingService.hpp | 54 +- src/libs/subsonic/CMakeLists.txt | 1 + .../impl/entrypoints/AlbumSongLists.cpp | 18 +- .../impl/entrypoints/MediaAnnotation.cpp | 13 +- src/libs/subsonic/impl/responses/Album.cpp | 3 +- src/libs/subsonic/impl/responses/Artist.cpp | 4 +- src/libs/subsonic/impl/responses/Song.cpp | 3 +- src/libs/utils/impl/Logger.cpp | 1 + src/libs/utils/impl/String.cpp | 113 +- src/libs/utils/include/utils/Logger.hpp | 1 + src/libs/utils/include/utils/String.hpp | 2 + src/lms/CMakeLists.txt | 1 + src/lms/main.cpp | 426 +++---- src/lms/ui/PlayQueue.cpp | 8 +- src/lms/ui/SettingsView.cpp | 1040 +++++++++-------- src/lms/ui/explore/ArtistCollector.cpp | 169 ++- src/lms/ui/explore/ArtistView.cpp | 8 +- src/lms/ui/explore/ReleaseCollector.cpp | 162 +-- src/lms/ui/explore/ReleaseView.cpp | 14 +- src/lms/ui/explore/TrackCollector.cpp | 11 +- src/lms/ui/explore/TrackListHelpers.cpp | 8 +- 89 files changed, 3763 insertions(+), 3616 deletions(-) create mode 100644 src/libs/services/feedback/CMakeLists.txt create mode 100644 src/libs/services/feedback/impl/FeedbackService.cpp create mode 100644 src/libs/services/feedback/impl/FeedbackService.hpp create mode 100644 src/libs/services/feedback/impl/FeedbackService.impl.hpp rename src/libs/services/{scrobbling/impl/IScrobbler.hpp => feedback/impl/IFeedbackBackend.hpp} (64%) create mode 100644 src/libs/services/feedback/impl/internal/InternalBackend.cpp rename src/libs/services/{scrobbling/impl/internal/InternalScrobbler.hpp => feedback/impl/internal/InternalBackend.hpp} (74%) create mode 100644 src/libs/services/feedback/impl/listenbrainz/Exception.hpp rename src/libs/services/{scrobbling => feedback}/impl/listenbrainz/FeedbackTypes.cpp (93%) rename src/libs/services/{scrobbling => feedback}/impl/listenbrainz/FeedbackTypes.hpp (94%) create mode 100644 src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.cpp rename src/libs/services/{scrobbling => feedback}/impl/listenbrainz/FeedbacksParser.hpp (93%) create mode 100644 src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp create mode 100644 src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.hpp create mode 100644 src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp create mode 100644 src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.hpp create mode 100644 src/libs/services/feedback/impl/listenbrainz/Utils.cpp create mode 100644 src/libs/services/feedback/impl/listenbrainz/Utils.hpp create mode 100644 src/libs/services/feedback/include/services/feedback/Exception.hpp create mode 100644 src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp create mode 100644 src/libs/services/scrobbling/impl/IScrobblingBackend.hpp delete mode 100644 src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp create mode 100644 src/libs/services/scrobbling/impl/internal/InternalBackend.cpp create mode 100644 src/libs/services/scrobbling/impl/internal/InternalBackend.hpp delete mode 100644 src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp delete mode 100644 src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.cpp delete mode 100644 src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp delete mode 100644 src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.hpp create mode 100644 src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.cpp create mode 100644 src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.hpp delete mode 100644 src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp delete mode 100644 src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp diff --git a/approot/messages.xml b/approot/messages.xml index 28883d34..4793931f 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -240,10 +240,14 @@ Audio These audio settings are local to your browser! +Internal +ListenBrainz +ListenBrainz API token Change password Default transcode bitrate Default transcode format Cannot save using a demo account! +Feedback Settings Bad password Old password must be filled in @@ -255,10 +259,7 @@ ReplayGain preamp ReplayGain preamp (if no info) Scrobbling -Scrobbler -Internal -ListenBrainz -ListenBrainz token +Services Settings Artist list mode All artists diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index a5f2e255..d7ace5e6 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -240,10 +240,14 @@ Audio Ces paramètres audio sont locaux à votre navigateur ! +Interne +ListenBrainz +Jeton d'API ListenBrainz Changement de mot de passe Bitrate par defaut du transcodage Format par defaut du transcodage Impossible de sauvegarder en utilisant un compte de démo ! +Feedback Paramètres Mauvais mot de passe L'ancien mot de passe doit être renseigné @@ -255,10 +259,7 @@ Pre-amplification ReplayGain Pre-amplification ReplayGain (si pas d'info) Scrobbling -Scrobbler -Interne -ListenBrainz -Jeton ListenBrainz +Services Paramètres Mode de listage des artistes Tous les artistes diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 7849ef42..67ebf67e 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -9,7 +9,7 @@ Annulla Tags Crea -Elimina +Elimina Annulla Modifica Caricamento... @@ -240,10 +240,14 @@ Audio Queste impostazioni sono salvate localmente su questo dispositivo! + + + Cambia password Non posso salvare le impostazioni utilizzando l'account demo! + Impostazioni Password errata Devi inserire la password attuale @@ -257,9 +261,6 @@ - - - Modalità di elencazione artisti Tutti gli artisti Artisti album diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index c1eca28c..317f85ea 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -240,10 +240,14 @@ 音频 这些音频设置仅保存在您的本地浏览器! + + + 更改密码 使用演示账号时不能保存! + 设置 无效密码 必须填写旧密码 @@ -257,9 +261,6 @@ - - - 歌手列表模式 所有歌手 专辑歌手 diff --git a/approot/settings.xml b/approot/settings.xml index c15db564..507ec686 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -28,9 +28,9 @@
-
${transcode-bitrate class="form-control"} kbps @@ -105,19 +105,28 @@
${} - ${tr:Lms.Settings.scrobbling} + ${tr:Lms.Settings.services}
-
+ + ${feedback-backend class="form-control"} +
+ ${feedback-backend-info class="help-block"} +
+
+
${listenbrainz-token class="form-control"}
diff --git a/src/libs/services/CMakeLists.txt b/src/libs/services/CMakeLists.txt index fd4d3160..70f360ed 100644 --- a/src/libs/services/CMakeLists.txt +++ b/src/libs/services/CMakeLists.txt @@ -1,6 +1,7 @@ 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/database/impl/Artist.cpp b/src/libs/services/database/impl/Artist.cpp index 1bbfbfeb..a1b33294 100644 --- a/src/libs/services/database/impl/Artist.cpp +++ b/src/libs/services/database/impl/Artist.cpp @@ -133,11 +133,11 @@ createQuery(Session& session, const Artist::FindParameters& params) if (params.starringUser.isValid()) { - assert(params.scrobbler); + assert(params.feedbackBackend); 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) - .where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); + .where("s_a.backend = ?").bind(*params.feedbackBackend) + .where("s_a.sync_state <> ?").bind(SyncState::PendingRemove); } if (!params.clusters.empty()) diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp index d77ef827..c1462384 100644 --- a/src/libs/services/database/impl/Listen.cpp +++ b/src/libs/services/database/impl/Listen.cpp @@ -29,14 +29,14 @@ namespace { using namespace Database; - Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType) + Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, 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) }; + .where("l.backend = ?").bind(backend) }; if (linkType) query.where("t_a_l.type = ?").bind(*linkType); @@ -66,13 +66,13 @@ namespace return query; } - Wt::Dbo::Query createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + Wt::Dbo::Query createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, 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) }; + .where("l.backend = ?").bind(backend) }; if (!clusterIds.empty()) { @@ -98,12 +98,12 @@ namespace return query; } - Wt::Dbo::Query createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds) + Wt::Dbo::Query createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, 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) }; + .where("l.backend = ?").bind(backend) }; if (!clusterIds.empty()) { @@ -131,16 +131,16 @@ namespace namespace Database { - Listen::Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + Listen::Listen(ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) : _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) } - , _scrobbler{ scrobbler } + , _backend{ backend } , _user{ getDboPtr(user) } , _track{ getDboPtr(track) } {} - Listen::pointer Listen::create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + Listen::pointer Listen::create(Session& session, ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) { - return session.getDboSession().add(std::unique_ptr {new Listen{ user, track, scrobbler, dateTime }}); + return session.getDboSession().add(std::unique_ptr {new Listen{ user, track, backend, dateTime }}); } std::size_t Listen::getCount(Session& session) @@ -165,30 +165,30 @@ namespace Database if (parameters.user.isValid()) query.where("user_id = ?").bind(parameters.user); - if (parameters.scrobbler) - query.where("scrobbler = ?").bind(*parameters.scrobbler); + if (parameters.backend) + query.where("backend = ?").bind(*parameters.backend); - if (parameters.scrobblingState) - query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); + if (parameters.syncState) + query.where("sync_state = ?").bind(*parameters.syncState); return Utils::execQuery(query, parameters.range); } - Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime) + Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, 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("backend = ?").bind(backend) .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) .resultValue(); } - RangeResults Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) { - auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) }; + auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) }; auto collection{ query .orderBy("COUNT(a.id) DESC") @@ -197,72 +197,72 @@ namespace Database return Utils::execQuery(query, range); } - RangeResults Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { - auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; return Utils::execQuery(query, range); } - RangeResults Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range) + RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { - auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; return Utils::execQuery(query, range); } - RangeResults Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, std::optional linkType, Range range) + RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) { - auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) + auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; return Utils::execQuery(query, range); } - RangeResults Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { - auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds) + auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; return Utils::execQuery(query, range); } - RangeResults Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector& clusterIds, Range range) + RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { - auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds) + auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; return Utils::execQuery(query, range); } - Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId) + Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) { // TODO not pending remove? return session.getDboSession().query>("SELECT l from listen l") .join("track t ON l.track_id = t.id") .where("t.release_id = ?").bind(releaseId) .where("l.user_id = ?").bind(userId) - .where("l.scrobbler = ?").bind(scrobbler) + .where("l.backend = ?").bind(backend) .orderBy("l.date_time DESC") .limit(1) .resultValue(); } - Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId trackId) + Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) { // TODO not pending remove? return session.getDboSession().query>("SELECT l from listen l") .join("track t ON track_id = t.id") .where("t.id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) - .where("l.scrobbler = ?").bind(scrobbler) + .where("l.backend = ?").bind(backend) .orderBy("l.date_time DESC") .limit(1) .resultValue(); diff --git a/src/libs/services/database/impl/Migration.cpp b/src/libs/services/database/impl/Migration.cpp index 6c11b887..da002ba0 100644 --- a/src/libs/services/database/impl/Migration.cpp +++ b/src/libs/services/database/impl/Migration.cpp @@ -72,414 +72,6 @@ namespace Database::Migration Db& _db; }; - static std::string dateTimeToDbFormat(const Wt::WDateTime& dateTime) - { - return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8(); - } - - static void migrateFromV5(Session& session) - { - session.getDboSession().execute("DELETE FROM auth_token"); // format has changed - } - - static void migrateFromV6(Session& session) - { - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - - static void migrateFromV7(Session& session) - { - session.getDboSession().execute("DROP TABLE similarity_settings"); - session.getDboSession().execute("DROP TABLE similarity_settings_feature"); - session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(ScanSettings::RecommendationEngineType::Clusters)) + ")"); - } - - static void migrateFromV8(Session& session) - { - // Better cover handling, need to rescan the whole files - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV9(Session& session) - { - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "track_bookmark" ( - "id" integer primary key autoincrement, - "version" integer not null, - "offset" integer, - "comment" text not null, - "track_id" bigint, - "user_id" bigint, - constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred, - constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred -);)"); - } - - static void migrateFromV10(Session& session) - { - ScanSettings::get(session).modify()->addAudioFileExtension(".m4b"); - ScanSettings::get(session).modify()->addAudioFileExtension(".alac"); - } - - static void migrateFromV11(Session& session) - { - // Sanitize bad MBID, need to rescan the whole files - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV12(Session& session) - { - // Artist and release that have a badly parsed name but a MBID had no chance to updat the name - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV13(Session& session) - { - // Always store UUID in lower case + better WMA parsing - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV14(Session& session) - { - // SortName now set from metadata - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV15(Session& session) - { - session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultUITheme)) + ")"); - } - - static void migrateFromV16(Session& session) - { - session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)"); - session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)"); - - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV17(Session& session) - { - // Drop colums total_disc/total_track from release - session.getDboSession().execute(R"( -CREATE TABLE "release_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "name" text not null, - "mbid" text not null -))"); - session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release"); - session.getDboSession().execute("DROP TABLE release"); - session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release"); - - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV18(Session& session) - { - session.getDboSession().execute(R"( -CREATE TABLE IF NOT EXISTS "subsonic_settings" ( - "id" integer primary key autoincrement, - "version" integer not null, - "api_enabled" boolean not null, - "artist_list_mode" integer not null -))"); - } - - static void migrateFromV19(Session& session) - { - session.getDboSession().execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - session.getDboSession().execute(std::string{ "INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, " } - + "1" // default enable transcode - + ", " + std::to_string(static_cast(User::defaultSubsonicTranscodeFormat)) - + ", " + std::to_string(User::defaultSubsonicTranscodeBitrate) - + ", " + std::to_string(static_cast(User::defaultSubsonicArtistListMode)) - + ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } - - static void migrateFromV20(Session& session) - { - session.getDboSession().execute("DROP TABLE subsonic_settings"); - } - - static void migrateFromV21(Session& session) - { - session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL"); - session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL"); - - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV22(Session& session) - { - session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); - - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV23(Session& session) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV24(Session& session) - { - // User's AuthMode - session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); - } - - static void migrateFromV25(Session& session) - { - // Better cover detection - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV26(Session& session) - { - // Composer, mixer, etc. support - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV27(Session& session) - { - // Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV28(Session& session) - { - // Drop Auth mode - session.getDboSession().execute(R"( -CREATE TABLE "user_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "type" integer not null, - "login_name" text not null, - "password_salt" text not null, - "password_hash" text not null, - "last_login" text, - "subsonic_transcode_enable" boolean not null, - "subsonic_transcode_format" integer not null, - "subsonic_transcode_bitrate" integer not null, - "subsonic_artist_list_mode" integer not null, - "ui_theme" integer not null, - "cur_playing_track_pos" integer not null, - "repeat_all" boolean not null, - "radio" boolean not null -))"); - session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); - session.getDboSession().execute("DROP TABLE user"); - session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user"); - } - - static void migrateFromV29(Session& session) - { - session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT"); - session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT"); - session.getDboSession().execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultScrobbler)) + ")"); - session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT"); - - session.getDboSession().execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__"); - - // MBID changes - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV30(Session& session) - { - // drop "year" and "original_year" (rescan needed to convert them into dates) - session.getDboSession().execute(R"( -CREATE TABLE "track_backup" ( - "id" integer primary key autoincrement, - "version" integer not null, - "scan_version" integer not null, - "track_number" integer not null, - "disc_number" integer not null, - "name" text not null, - "duration" integer, - "date" integer text, - "original_date" integer text, - "file_path" text not null, - "file_last_write" text, - "file_added" text, - "has_cover" boolean not null, - "mbid" text not null, - "copyright" text not null, - "copyright_url" text not null, - "release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT, - constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred -))"); - session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track"); - session.getDboSession().execute("DROP TABLE track"); - session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); - - // Just increment the scan version of the settings to make the next scheduled scan rescan everything - ScanSettings::get(session).modify()->incScanVersion(); - } - - static void migrateFromV31(Session& session) - { - // new star system, using dedicated entries per scrobbler and date time - session.getDboSession().execute(R"( -CREATE TABLE "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.getDboSession().execute(R"( -CREATE TABLE "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.getDboSession().execute(R"( -CREATE TABLE "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 -))"); - - // Can't migrate using class mapping as mapping may evolve in the future - - // use time_t to avoid rounding issues later - const std::string now{ dateTimeToDbFormat(Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t())) }; - - std::map userScrobblers; - auto getScrobbler{ [&](IdType::ValueType userId) - { - auto itScrobbler {userScrobblers.find(userId)}; - if (itScrobbler != std::cend(userScrobblers)) - return itScrobbler->second; - - auto query {session.getDboSession().query("SELECT scrobbler FROM user WHERE id = ?").bind(userId)}; - [[maybe_unused]] auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())}; - assert(inserted); - return itInserted->second; - } }; - - auto migrateStarEntries{ [&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName) - { - using UserIdObjectId = std::tuple; - - std::vector starredEntries; - auto query {session.getDboSession().query("SELECT user_id, " + colName + " from " + oldTableName)}; - auto results {query.resultList()}; - - LMS_LOG(DB, INFO) << "Found " << results.size() << " " << colName << " to migrate"; - - for (const auto& [userId, entryId] : results) - { - session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)") - .bind(0) - .bind(getScrobbler(userId)) - .bind(now) - .bind(entryId) - .bind(userId); - } - - session.getDboSession().execute("DROP TABLE " + oldTableName); - } }; - - migrateStarEntries("artist_id", "user_artist_starred", "starred_artist"); - migrateStarEntries("release_id", "user_release_starred", "starred_release"); - migrateStarEntries("track_id", "user_track_starred", "starred_track"); - - // new listen system, no longer using tracklists - session.getDboSession().execute(R"( -CREATE TABLE "listen" ( - "id" integer primary key autoincrement, - "version" integer not null, - "date_time" text, - "scrobbler" integer not null, - "scrobbling_state" 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 -))"); - - auto migrateListens{ [&session](const std::string& trackListName, Scrobbler scrobbler) - { - using UserIdObjectId = std::tuple; - - std::vector listens; - auto query {session.getDboSession().query("SELECT t_l.user_id, t_l_e.track_id, t_l_e.date_time FROM tracklist t_l") - .join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id") - .where("t_l.name = ?").bind(trackListName)}; - auto results {query.resultList()}; - listens.reserve(results.size()); - - LMS_LOG(DB, INFO) << "Found " << results.size() << " listens in " << trackListName; - - for (const auto& [userId, trackId, dateTime] : results) - { - session.getDboSession().execute("INSERT INTO listen ('version', 'date_time', 'scrobbler', 'scrobbling_state', 'track_id', 'user_id') VALUES (?, ?, ?, ?, ?, ?)") - .bind(0) - .bind(dateTimeToDbFormat(dateTime)) - .bind(scrobbler) - .bind(ScrobblingState::Synchronized) // consider sync is done to avoid duplicate submissions - .bind(trackId) - .bind(userId); - } - } }; - - migrateListens("__scrobbler_internal_history__", Scrobbler::Internal); - migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz); - } - static void migrateFromV32(Session& session) { ScanSettings::get(session).modify()->addAudioFileExtension(".wv"); @@ -605,6 +197,33 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); } + static void migrateFromV42(Session& session) + { + session.getDboSession().execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); + session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); + + // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) + session.getDboSession().execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); + session.getDboSession().execute("ALTER TABLE user ADD feedback_backend INTEGER"); + session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); + session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); + session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); + session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); + session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); + session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); + + session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend"); + } + void doDbMigration(Session& session) { static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; @@ -615,33 +234,6 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( const std::map migrationFunctions { - {5, migrateFromV5}, - {6, migrateFromV6}, - {7, migrateFromV7}, - {8, migrateFromV8}, - {9, migrateFromV9}, - {10, migrateFromV10}, - {11, migrateFromV11}, - {12, migrateFromV12}, - {13, migrateFromV13}, - {14, migrateFromV14}, - {15, migrateFromV15}, - {16, migrateFromV16}, - {17, migrateFromV17}, - {18, migrateFromV18}, - {19, migrateFromV19}, - {20, migrateFromV20}, - {21, migrateFromV21}, - {22, migrateFromV22}, - {23, migrateFromV23}, - {24, migrateFromV24}, - {25, migrateFromV25}, - {26, migrateFromV26}, - {27, migrateFromV27}, - {28, migrateFromV28}, - {29, migrateFromV29}, - {30, migrateFromV30}, - {31, migrateFromV31}, {32, migrateFromV32}, {33, migrateFromV33}, {34, migrateFromV34}, @@ -652,6 +244,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" ( {39, migrateFromV39}, {40, migrateFromV40}, {41, migrateFromV41}, + {42, migrateFromV42}, }; { diff --git a/src/libs/services/database/impl/Migration.hpp b/src/libs/services/database/impl/Migration.hpp index 68a70ade..55b1e679 100644 --- a/src/libs/services/database/impl/Migration.hpp +++ b/src/libs/services/database/impl/Migration.hpp @@ -26,7 +26,7 @@ namespace Database class Session; using Version = std::size_t; - static constexpr Version LMS_DATABASE_VERSION{ 42 }; + static constexpr Version LMS_DATABASE_VERSION{ 43 }; class VersionInfo { public: diff --git a/src/libs/services/database/impl/Release.cpp b/src/libs/services/database/impl/Release.cpp index 109c24b7..a04bf9ab 100644 --- a/src/libs/services/database/impl/Release.cpp +++ b/src/libs/services/database/impl/Release.cpp @@ -64,11 +64,11 @@ namespace Database if (params.starringUser.isValid()) { - assert(params.scrobbler); + assert(params.feedbackBackend); 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) - .where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); + .where("s_r.backend = ?").bind(*params.feedbackBackend) + .where("s_r.sync_state <> ?").bind(SyncState::PendingRemove); } if (params.artist.isValid()) diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp index 90387aaa..09120b50 100644 --- a/src/libs/services/database/impl/Session.cpp +++ b/src/libs/services/database/impl/Session.cpp @@ -158,15 +158,15 @@ namespace Database _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_scrobbler_date_time_idx ON listen(user_id,track_id,scrobbler,date_time)"); - _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_artist_artist_user_scrobbler_idx ON starred_artist(artist_id,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_release_release_user_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); } // Initial settings tables diff --git a/src/libs/services/database/impl/StarredArtist.cpp b/src/libs/services/database/impl/StarredArtist.cpp index ca4b3638..0427c80b 100644 --- a/src/libs/services/database/impl/StarredArtist.cpp +++ b/src/libs/services/database/impl/StarredArtist.cpp @@ -29,47 +29,42 @@ namespace Database { - StarredArtist::StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) - : _scrobbler {scrobbler} - , _artist {getDboPtr(artist)} - , _user {getDboPtr(user)} - { - } + StarredArtist::StarredArtist(ObjectPtr artist, ObjectPtr user, FeedbackBackend backend) + : _backend{ backend } + , _artist{ getDboPtr(artist) } + , _user{ getDboPtr(user) } + { + } - StarredArtist::pointer - StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler) - { - return session.getDboSession().add(std::unique_ptr {new StarredArtist {artist, user, scrobbler}}); - } + StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr artist, ObjectPtr user, FeedbackBackend backend) + { + return session.getDboSession().add(std::unique_ptr {new StarredArtist{ artist, user, backend }}); + } - std::size_t - StarredArtist::getCount(Session& session) - { - session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_artist"); - } + 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, 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::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("artist_id = ?").bind(artistId) + .where("user_id = ?").bind(userId) + .where("backend = ?").bind(backend) + .resultValue(); + } - void - StarredArtist::setDateTime(const Wt::WDateTime& dateTime) - { - _dateTime = Utils::normalizeDateTime(dateTime); - } + void StarredArtist::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = Utils::normalizeDateTime(dateTime); + } } diff --git a/src/libs/services/database/impl/StarredRelease.cpp b/src/libs/services/database/impl/StarredRelease.cpp index 17f1c598..7645d3f9 100644 --- a/src/libs/services/database/impl/StarredRelease.cpp +++ b/src/libs/services/database/impl/StarredRelease.cpp @@ -29,47 +29,42 @@ namespace Database { - StarredRelease::StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) - : _scrobbler {scrobbler} - , _release {getDboPtr(release)} - , _user {getDboPtr(user)} - { - } + StarredRelease::StarredRelease(ObjectPtr release, ObjectPtr user, FeedbackBackend backend) + : _backend{ backend } + , _release{ getDboPtr(release) } + , _user{ getDboPtr(user) } + { + } - StarredRelease::pointer - StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler) - { - return session.getDboSession().add(std::unique_ptr{new StarredRelease{release, user, scrobbler}}); - } + StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr release, ObjectPtr user, FeedbackBackend backend) + { + return session.getDboSession().add(std::unique_ptr{new StarredRelease{ release, user, backend }}); + } - std::size_t - StarredRelease::getCount(Session& session) - { - session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_release"); - } + 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, 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::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("release_id = ?").bind(releaseId) + .where("user_id = ?").bind(userId) + .where("backend = ?").bind(backend) + .resultValue(); + } - void - StarredRelease::setDateTime(const Wt::WDateTime& dateTime) - { - _dateTime = Utils::normalizeDateTime(dateTime); - } + void StarredRelease::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = Utils::normalizeDateTime(dateTime); + } } diff --git a/src/libs/services/database/impl/StarredTrack.cpp b/src/libs/services/database/impl/StarredTrack.cpp index 21158c9a..2f03fbe6 100644 --- a/src/libs/services/database/impl/StarredTrack.cpp +++ b/src/libs/services/database/impl/StarredTrack.cpp @@ -29,64 +29,58 @@ namespace Database { - StarredTrack::StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) - : _scrobbler {scrobbler} - , _track {getDboPtr(track)} - , _user {getDboPtr(user)} - { - } + StarredTrack::StarredTrack(ObjectPtr track, ObjectPtr user, FeedbackBackend backend) + : _backend{ backend } + , _track{ getDboPtr(track) } + , _user{ getDboPtr(user) } + { + } - StarredTrack::pointer - StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler) - { - return session.getDboSession().add(std::unique_ptr {new StarredTrack{track, user, scrobbler}}); - } + StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr track, ObjectPtr user, FeedbackBackend backend) + { + return session.getDboSession().add(std::unique_ptr {new StarredTrack{ track, user, backend }}); + } - std::size_t - StarredTrack::getCount(Session& session) - { - session.checkSharedLocked(); - return session.getDboSession().query("SELECT COUNT(*) FROM starred_track"); - } + 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, 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::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) + { + session.checkSharedLocked(); + return session.getDboSession().find() + .where("track_id = ?").bind(trackId) + .where("user_id = ?").bind(userId) + .where("backend = ?").bind(backend) + .resultValue(); + } - RangeResults - StarredTrack::find(Session& session, const FindParameters& params) - { - session.checkSharedLocked(); + RangeResults StarredTrack::find(Session& session, const FindParameters& params) + { + session.checkSharedLocked(); - auto query {session.getDboSession().query("SELECT DISTINCT s_t.id FROM starred_track s_t")}; + auto query{ session.getDboSession().query("SELECT DISTINCT s_t.id FROM starred_track s_t") }; - if (params.scrobbler) - query.where("s_t.scrobbler = ?").bind(*params.scrobbler); - if (params.scrobblingState) - query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState); - if (params.user.isValid()) - query.where("s_t.user_id = ?").bind(params.user); + if (params.backend) + query.where("s_t.backend = ?").bind(*params.backend); + if (params.syncState) + query.where("s_t.sync_state = ?").bind(*params.syncState); + if (params.user.isValid()) + query.where("s_t.user_id = ?").bind(params.user); - return Utils::execQuery(query, params.range); - } + return Utils::execQuery(query, params.range); + } - void - StarredTrack::setDateTime(const Wt::WDateTime& dateTime) - { - _dateTime = Utils::normalizeDateTime(dateTime); - } + void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) + { + _dateTime = Utils::normalizeDateTime(dateTime); + } } diff --git a/src/libs/services/database/impl/Track.cpp b/src/libs/services/database/impl/Track.cpp index 993acdc1..5f3933e5 100644 --- a/src/libs/services/database/impl/Track.cpp +++ b/src/libs/services/database/impl/Track.cpp @@ -57,11 +57,11 @@ createQuery(Session& session, const Track::FindParameters& params) if (params.starringUser.isValid()) { - assert(params.scrobbler); + assert(params.feedbackBackend); 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) - .where("s_t.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); + .where("s_t.backend = ?").bind(*params.feedbackBackend) + .where("s_t.sync_state <> ?").bind(SyncState::PendingRemove); } if (!params.clusters.empty()) diff --git a/src/libs/services/database/impl/User.cpp b/src/libs/services/database/impl/User.cpp index 0661ea69..926b4ac3 100644 --- a/src/libs/services/database/impl/User.cpp +++ b/src/libs/services/database/impl/User.cpp @@ -53,8 +53,10 @@ namespace Database { auto query{ session.getDboSession().query("SELECT id FROM user") }; - if (params.scrobbler) - query.where("scrobbler = ?").bind(*params.scrobbler); + if (params.scrobblingBackend) + query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend); + if (params.feedbackBackend) + query.where("feedback_backend = ?").bind(*params.feedbackBackend); return Utils::execQuery(query, params.range); } diff --git a/src/libs/services/database/include/services/database/Artist.hpp b/src/libs/services/database/include/services/database/Artist.hpp index c0c374b3..40331f9d 100644 --- a/src/libs/services/database/include/services/database/Artist.hpp +++ b/src/libs/services/database/include/services/database/Artist.hpp @@ -61,7 +61,7 @@ class Artist final : public Object Range range; Wt::WDateTime writtenAfter; UserId starringUser; // only artists starred by this user - std::optional scrobbler; // and for this scrobbler + std::optional feedbackBackend; // and for this feedback backend TrackId track; // artists involved in this track ReleaseId release; // artists involved in this release @@ -71,7 +71,7 @@ class Artist final : public Object 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; } + FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } }; diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp index 231d763e..b61ac2ec 100644 --- a/src/libs/services/database/include/services/database/Listen.hpp +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -46,47 +46,47 @@ namespace Database struct FindParameters { UserId user; - std::optional scrobbler; - std::optional scrobblingState; + std::optional backend; + std::optional syncState; Range range; FindParameters& setUser(UserId _user) { user = _user; return *this; } - FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } - FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; } + FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; } + FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; } }; // Accessors static std::size_t getCount(Session& session); static pointer find(Session& session, ListenId id); - static pointer find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + static pointer find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime); static RangeResults find(Session& session, const FindParameters& parameters); // 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 getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); + static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, 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 = {}); + static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range = {}); + static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); - static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId); - static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId); + static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId); + static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId); - ScrobblingState getScrobblingState() const { return _scrobblingState; } + SyncState getSyncState() const { return _syncState; } ObjectPtr getUser() const { return _user; } ObjectPtr getTrack() const { return _track; } const Wt::WDateTime& getDateTime() const { return _dateTime; } - void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } + void setSyncState(SyncState state) { _syncState = state; } template void persist(Action& a) { Wt::Dbo::field(a, _dateTime, "date_time"); - Wt::Dbo::field(a, _scrobbler, "scrobbler"); - Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); + Wt::Dbo::field(a, _backend, "backend"); // TODO rename + Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); @@ -94,12 +94,12 @@ namespace Database private: friend class Session; - Listen(ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); - static pointer create(Session& session, ObjectPtr user, ObjectPtr track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); + Listen(ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime); + static pointer create(Session& session, ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime); - Wt::WDateTime _dateTime; - Scrobbler _scrobbler; - ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd }; + Wt::WDateTime _dateTime; + ScrobblingBackend _backend; + SyncState _syncState{ SyncState::PendingAdd }; Wt::Dbo::ptr _user; Wt::Dbo::ptr _track; diff --git a/src/libs/services/database/include/services/database/Release.hpp b/src/libs/services/database/include/services/database/Release.hpp index a9c39b07..dce8f4f3 100644 --- a/src/libs/services/database/include/services/database/Release.hpp +++ b/src/libs/services/database/include/services/database/Release.hpp @@ -57,7 +57,7 @@ namespace Database Wt::WDateTime writtenAfter; std::optional dateRange; UserId starringUser; // only releases starred by this user - std::optional scrobbler; // and for this scrobbler + std::optional feedbackBackend; // and for this backend ArtistId artist; // only releases that involved this user EnumSet trackArtistLinkTypes; // and for these link types EnumSet excludedTrackArtistLinkTypes; // but not for these link types @@ -70,7 +70,7 @@ namespace Database 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; } + FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}, EnumSet _excludedTrackArtistLinkTypes = {}) { artist = _artist; @@ -104,7 +104,7 @@ namespace Database std::optional getCopyrightURL() const; // Accessors - const std::string& getName() const { return _name; } + const std::string& getName() const { return _name; } std::optional getMBID() const { return UUID::fromString(_MBID); } std::optional getTotalDisc() const { return _totalDisc; } std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) diff --git a/src/libs/services/database/include/services/database/StarredArtist.hpp b/src/libs/services/database/include/services/database/StarredArtist.hpp index b4f48b6b..1d02eaf5 100644 --- a/src/libs/services/database/include/services/database/StarredArtist.hpp +++ b/src/libs/services/database/include/services/database/StarredArtist.hpp @@ -30,53 +30,53 @@ namespace Database { - class Artist; - class Session; - class User; + class Artist; + class Session; + class User; - class StarredArtist final : public Object - { - public: - StarredArtist() = default; + class StarredArtist final : public Object + { + public: + StarredArtist() = default; - // 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); + // 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, FeedbackBackend backend); - // Accessors - ObjectPtr getArtist() const { return _artist; } - ObjectPtr getUser() const { return _user; } - Scrobbler getScrobbler() const { return _scrobbler; } - const Wt::WDateTime& getDateTime() const { return _dateTime; } - ScrobblingState getScrobblingState() const { return _scrobblingState; } + // Accessors + ObjectPtr getArtist() const { return _artist; } + ObjectPtr getUser() const { return _user; } + FeedbackBackend getFeedbackBackend() const { return _backend; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + SyncState getSyncState() const { return _syncState; } - // Setters - void setDateTime(const Wt::WDateTime& dateTime); - void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + void setSyncState(SyncState state) { _syncState = state; } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _scrobbler, "scrobbler"); - Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); - Wt::Dbo::field(a, _dateTime, "date_time"); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _backend, "backend"); + Wt::Dbo::field(a, _syncState, "sync_state"); + 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); - } + Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } - private: - friend class Session; - StarredArtist(ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); - static pointer create(Session& session, ObjectPtr artist, ObjectPtr user, Scrobbler scrobbler); + private: + friend class Session; + StarredArtist(ObjectPtr artist, ObjectPtr user, FeedbackBackend scrobblingbackend); + static pointer create(Session& session, ObjectPtr artist, ObjectPtr user, FeedbackBackend scrobblingbackend); - Scrobbler _scrobbler; // for which scrobbler - ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; - Wt::WDateTime _dateTime; // when it was starred + FeedbackBackend _backend; // for which backend + SyncState _syncState{ SyncState::PendingAdd }; + Wt::WDateTime _dateTime; // when it was starred - Wt::Dbo::ptr _artist; - Wt::Dbo::ptr _user; - }; + 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 index f3b4280a..23d4116e 100644 --- a/src/libs/services/database/include/services/database/StarredRelease.hpp +++ b/src/libs/services/database/include/services/database/StarredRelease.hpp @@ -30,53 +30,53 @@ namespace Database { - class Release; - class Session; - class User; + class Release; + class Session; + class User; - class StarredRelease final : public Object - { - public: - StarredRelease() = default; + class StarredRelease final : public Object + { + public: + StarredRelease() = default; - // 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); + // 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, FeedbackBackend backend); - // Accessors - ObjectPtr getRelease() const { return _release; } - ObjectPtr getUser() const { return _user; } - Scrobbler getScrobbler() const { return _scrobbler; } - const Wt::WDateTime& getDateTime() const { return _dateTime; } - ScrobblingState getScrobblingState() const { return _scrobblingState; } + // Accessors + ObjectPtr getRelease() const { return _release; } + ObjectPtr getUser() const { return _user; } + FeedbackBackend getFeedbackBackend() const { return _backend; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + SyncState getSyncState() const { return _syncState; } - // Setters - void setDateTime(const Wt::WDateTime& dateTime); - void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + void setSyncState(SyncState state) { _syncState = state; } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _scrobbler, "scrobbler"); - Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); - Wt::Dbo::field(a, _dateTime, "date_time"); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _backend, "backend"); + Wt::Dbo::field(a, _syncState, "sync_state"); + 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); - } + Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } - private: - friend class Session; - StarredRelease(ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); - static pointer create(Session& session, ObjectPtr release, ObjectPtr user, Scrobbler scrobbler); + private: + friend class Session; + StarredRelease(ObjectPtr release, ObjectPtr user, FeedbackBackend backend); + static pointer create(Session& session, ObjectPtr release, ObjectPtr user, FeedbackBackend backend); - Scrobbler _scrobbler; // for which scrobbler - ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; - Wt::WDateTime _dateTime; // when it was starred + FeedbackBackend _backend; // for which backend + SyncState _syncState{ SyncState::PendingAdd }; + Wt::WDateTime _dateTime; // when it was starred - Wt::Dbo::ptr _release; - Wt::Dbo::ptr _user; - }; + 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 index 197c88c1..7b3e50f7 100644 --- a/src/libs/services/database/include/services/database/StarredTrack.hpp +++ b/src/libs/services/database/include/services/database/StarredTrack.hpp @@ -31,66 +31,66 @@ namespace Database { - class Track; - class Session; - class User; + class Track; + class Session; + class User; - class StarredTrack final : public Object - { - public: - StarredTrack() = default; + class StarredTrack final : public Object + { + public: + StarredTrack() = default; - struct FindParameters - { - std::optional scrobbler; // for this scrobbler - std::optional scrobblingState; // and these states - UserId user; // and this user - Range range; + struct FindParameters + { + std::optional backend; // for this backend + std::optional syncState; // and these states + UserId user; // and this user + Range range; - FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; } - FindParameters& setUser(UserId _user) {user = _user; return *this; } - FindParameters& setRange(Range _range) {range = _range; return *this; } - }; + FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; } + FindParameters& setUser(UserId _user) { user = _user; return *this; } + FindParameters& setRange(Range _range) { range = _range; return *this; } + }; - // 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); - static RangeResults find(Session& session, const FindParameters& findParams); + // 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, FeedbackBackend backend); + static RangeResults find(Session& session, const FindParameters& findParams); - // Accessors - ObjectPtr getTrack() const { return _track; } - ObjectPtr getUser() const { return _user; } - Scrobbler getScrobbler() const { return _scrobbler; } - const Wt::WDateTime& getDateTime() const { return _dateTime; } - ScrobblingState getScrobblingState() const { return _scrobblingState; } + // Accessors + ObjectPtr getTrack() const { return _track; } + ObjectPtr getUser() const { return _user; } + FeedbackBackend getBackend() const { return _backend; } + const Wt::WDateTime& getDateTime() const { return _dateTime; } + SyncState getSyncState() const { return _syncState; } - // Setters - void setDateTime(const Wt::WDateTime& dateTime); - void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } + // Setters + void setDateTime(const Wt::WDateTime& dateTime); + void setSyncState(SyncState state) { _syncState = state; } - template - void persist(Action& a) - { - Wt::Dbo::field(a, _scrobbler, "scrobbler"); - Wt::Dbo::field(a, _scrobblingState, "scrobbling_state"); - Wt::Dbo::field(a, _dateTime, "date_time"); + template + void persist(Action& a) + { + Wt::Dbo::field(a, _backend, "backend"); + Wt::Dbo::field(a, _syncState, "sync_state"); + 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); - } + Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } - private: - friend class Session; - StarredTrack(ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); - static pointer create(Session& session, ObjectPtr track, ObjectPtr user, Scrobbler scrobbler); + private: + friend class Session; + StarredTrack(ObjectPtr track, ObjectPtr user, FeedbackBackend backend); + static pointer create(Session& session, ObjectPtr track, ObjectPtr user, FeedbackBackend backend); - Scrobbler _scrobbler; // for which scrobbler - ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; - Wt::WDateTime _dateTime; // when it was starred + FeedbackBackend _backend; // for which backend + SyncState _syncState{ SyncState::PendingAdd }; + Wt::WDateTime _dateTime; // when it was starred - Wt::Dbo::ptr _track; - Wt::Dbo::ptr _user; - }; + 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 94401a61..d95eee0a 100644 --- a/src/libs/services/database/include/services/database/Track.hpp +++ b/src/libs/services/database/include/services/database/Track.hpp @@ -61,23 +61,23 @@ class Track final : 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 - std::string name; // if non empty, must match this name - TrackSortMethod sortMethod {TrackSortMethod::None}; - Range range; - Wt::WDateTime writtenAfter; - UserId starringUser; // only tracks starred by this user - std::optional scrobbler; // and for this scrobbler - ArtistId artist; // only tracks that involve this artist - std::string artistName; // only tracks that involve this artist name - EnumSet trackArtistLinkTypes; // and for these link types - bool nonRelease {}; // only tracks that do not belong to a release - ReleaseId release; // matching this release - std::string releaseName; // matching this release name - TrackListId trackList; // matching this trackList - std::optional trackNumber; // matching this track number - bool distinct {true}; + 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 + std::string name; // if non empty, must match this name + TrackSortMethod sortMethod {TrackSortMethod::None}; + Range range; + Wt::WDateTime writtenAfter; + UserId starringUser; // only tracks starred by this user + std::optional feedbackBackend; // and for this feedback backend + ArtistId artist; // only tracks that involve this artist + std::string artistName; // only tracks that involve this artist name + EnumSet trackArtistLinkTypes; // and for these link types + bool nonRelease {}; // only tracks that do not belong to a release + ReleaseId release; // matching this release + std::string releaseName; // matching this release name + TrackListId trackList; // matching this trackList + std::optional trackNumber; // matching this track number + bool distinct {true}; FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; } FindParameters& setKeywords(const std::vector& _keywords) { keywords = _keywords; return *this; } @@ -85,7 +85,7 @@ class Track final : public Object 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; } + FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setArtist(ArtistId _artist, EnumSet _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtistName(std::string_view _artistName, EnumSet _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } diff --git a/src/libs/services/database/include/services/database/Types.hpp b/src/libs/services/database/include/services/database/Types.hpp index 3266c582..f618e8af 100644 --- a/src/libs/services/database/include/services/database/Types.hpp +++ b/src/libs/services/database/include/services/database/Types.hpp @@ -160,13 +160,19 @@ namespace Database void visitAllowedAudioBitrates(std::function); bool isAudioBitrateAllowed(Bitrate bitrate); - enum class Scrobbler + enum class ScrobblingBackend { Internal = 0, ListenBrainz = 1, }; - enum class ScrobblingState + enum class FeedbackBackend + { + Internal = 0, + ListenBrainz = 1, + }; + + enum class SyncState { PendingAdd = 0, Synchronized = 1, diff --git a/src/libs/services/database/include/services/database/User.hpp b/src/libs/services/database/include/services/database/User.hpp index 5a3bd8ee..f177120f 100644 --- a/src/libs/services/database/include/services/database/User.hpp +++ b/src/libs/services/database/include/services/database/User.hpp @@ -47,20 +47,23 @@ namespace Database { struct FindParameters { - std::optional scrobbler; - Range range; + std::optional scrobblingBackend; + std::optional feedbackBackend; + Range range; - FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; } + FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; } + FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; } }; - static inline const std::size_t MinNameLength{ 3 }; - static inline const std::size_t MaxNameLength{ 15 }; - 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 }; - static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal }; + static inline constexpr std::size_t MinNameLength{ 3 }; + static inline constexpr std::size_t MaxNameLength{ 15 }; + static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; + static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; + static inline constexpr UITheme defaultUITheme{ UITheme::Dark }; + static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; + static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal }; + static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal }; User() = default; @@ -88,7 +91,8 @@ namespace Database { void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void clearAuthTokens(); void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; } - void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; } + void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; } + void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; } void setListenBrainzToken(const std::optional& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; } // read @@ -102,7 +106,8 @@ namespace Database { bool isRadioSet() const { return _radio; } UITheme getUITheme() const { return _uiTheme; } SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } - Scrobbler getScrobbler() const { return _scrobbler; } + FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; } + ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; } std::optional getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); } template @@ -117,7 +122,8 @@ namespace Database { Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _uiTheme, "ui_theme"); - Wt::Dbo::field(a, _scrobbler, "scrobbler"); + Wt::Dbo::field(a, _feedbackBackend, "feedback_backend"); + Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend"); Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token"); // UI player settings @@ -138,7 +144,8 @@ namespace Database { std::string _passwordHash; Wt::WDateTime _lastLogin; UITheme _uiTheme{ defaultUITheme }; - Scrobbler _scrobbler{ defaultScrobbler }; + FeedbackBackend _feedbackBackend{ defaultFeedbackBackend }; + ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend }; std::string _listenbrainzToken; // Musicbrainz Identifier // Admin defined settings diff --git a/src/libs/services/database/test/Listen.cpp b/src/libs/services/database/test/Listen.cpp index 1a821711..66da627d 100644 --- a/src/libs/services/database/test/Listen.cpp +++ b/src/libs/services/database/test/Listen.cpp @@ -35,7 +35,7 @@ TEST_F(DatabaseFixture, Listen_getAll) 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}} }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; { auto transaction{ session.createSharedTransaction() }; @@ -45,7 +45,7 @@ TEST_F(DatabaseFixture, Listen_getAll) { 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}} }; + ScopedListen listen2{ session, user.get(), track.get(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}} }; EXPECT_EQ(Listen::getCount(session), 2); } @@ -61,12 +61,12 @@ 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}} }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; { auto transaction{ session.createSharedTransaction() }; - auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::ListenBrainz)) }; + auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::ListenBrainz)) }; EXPECT_EQ(listens.results.size(), 0); } @@ -74,18 +74,18 @@ TEST_F(DatabaseFixture, Listen_get) auto transaction{ session.createSharedTransaction() }; { - auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal)) }; + auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) }; EXPECT_EQ(listens.moreResults, false); ASSERT_EQ(listens.results.size(), 1); EXPECT_EQ(listens.results.front(), listen->getId()); } { - auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::PendingAdd)) }; + auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::PendingAdd)) }; EXPECT_EQ(listens.results.size(), 1); } { - auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal).setScrobblingState(ScrobblingState::Synchronized)) }; + auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal).setSyncState(SyncState::Synchronized)) }; EXPECT_EQ(listens.results.size(), 0); } } @@ -95,14 +95,14 @@ 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}} }; + ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; + ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}} }; { auto transaction{ session.createSharedTransaction() }; - auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobbler(Scrobbler::Internal)) }; + auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) }; ASSERT_EQ(listens.results.size(), 3); EXPECT_EQ(listens.results[0], listen1.getId()); EXPECT_EQ(listens.results[1], listen2.getId()); @@ -118,26 +118,26 @@ TEST_F(DatabaseFixture, Listen_get_byDateTime) 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 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 }; { auto transaction{ session.createSharedTransaction() }; { - Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime1) }; + Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime1) }; ASSERT_TRUE(listen); EXPECT_EQ(listen->getId(), listen1.getId()); } { - Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), Scrobbler::Internal, dateTime2) }; + Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::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)) }; + Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime2.addSecs(56)) }; EXPECT_FALSE(listen); } } @@ -148,19 +148,19 @@ 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 }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) }; { auto transaction{ session.createUniqueTransaction() }; @@ -171,20 +171,20 @@ TEST_F(DatabaseFixture, Listen_getTopArtists) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::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) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 0); } { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) }; EXPECT_EQ(artists.results.size(), 0); } { @@ -194,7 +194,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) }; EXPECT_EQ(artists.results.size(), 0); } } @@ -218,24 +218,24 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) }; EXPECT_EQ(artists.results.size(), 0); } - ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(3) }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 2); EXPECT_EQ(artists.results[0], artist2->getId()); EXPECT_EQ(artists.results[1], artist1->getId()); @@ -243,7 +243,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1}) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.moreResults, true); EXPECT_EQ(artists.results[0], artist2->getId()); @@ -256,7 +256,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) 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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; @@ -268,7 +268,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 0); } { @@ -278,7 +278,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getTopArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; + auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results[0], artist.getId()); } @@ -298,17 +298,17 @@ TEST_F(DatabaseFixture, Listen_getTopReleases) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.results.size(), 0); } - ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(releases.moreResults, false); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results[0], release.getId()); @@ -316,7 +316,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::ListenBrainz, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) }; EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.results.size(), 0); } @@ -328,7 +328,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi) 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 }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release2{ session, "MyRelease2" }; @@ -341,28 +341,28 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(releases.moreResults, false); ASSERT_EQ(releases.results.size(), 2); EXPECT_EQ(releases.results[0], release1.getId()); @@ -375,7 +375,7 @@ 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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedRelease release{ session, "MyRelease" }; @@ -388,7 +388,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(releases.results.size(), 0); } { @@ -399,7 +399,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getTopReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results[0], release.getId()); } @@ -414,17 +414,17 @@ TEST_F(DatabaseFixture, Listen_getTopTracks) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(tracks.moreResults, false); ASSERT_EQ(tracks.results.size(), 0); } - ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(tracks.moreResults, false); ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results[0], track.getId()); @@ -432,7 +432,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::ListenBrainz, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) }; EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.results.size(), 0); } @@ -444,33 +444,33 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi) 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 }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; + ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(tracks.moreResults, false); ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], track1.getId()); @@ -483,14 +483,14 @@ 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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::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()}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(tracks.results.size(), 0); } { @@ -501,7 +501,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getTopTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results[0], track.getId()); } @@ -521,31 +521,31 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::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) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 0); } { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, TrackArtistLinkType::Producer) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) }; EXPECT_EQ(artists.results.size(), 0); } { @@ -555,7 +555,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster->getId()}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) }; EXPECT_EQ(artists.results.size(), 0); } } @@ -579,32 +579,32 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) }; EXPECT_EQ(artists.results.size(), 0); } - ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(1) }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::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 listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen3{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {}, std::nullopt, Range {0, 1}) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.moreResults, true); EXPECT_EQ(artists.results[0], artist2->getId()); @@ -617,7 +617,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) 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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedClusterType clusterType{ session, "MyType" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; @@ -629,7 +629,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 0); } { @@ -639,7 +639,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) { auto transaction{ session.createSharedTransaction() }; - auto artists{ Listen::getRecentArtists(session, user->getId(), Scrobbler::Internal, {cluster.getId()}, std::nullopt) }; + auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) }; ASSERT_EQ(artists.results.size(), 1); EXPECT_EQ(artists.results[0], artist.getId()); } @@ -659,18 +659,18 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(releases.moreResults, false); ASSERT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results[0], release.getId()); @@ -678,7 +678,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::ListenBrainz, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) }; EXPECT_EQ(releases.moreResults, false); EXPECT_EQ(releases.results.size(), 0); } @@ -699,39 +699,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease) { auto transaction{ session.createSharedTransaction() }; - const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; + const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; EXPECT_FALSE(listen); } const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; { auto transaction{ session.createSharedTransaction() }; - const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; + const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime1); } const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 }; + ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 }; { auto transaction{ session.createSharedTransaction() }; - const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; + const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime1); } const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 }; + ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 }; { auto transaction{ session.createSharedTransaction() }; - const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) }; + const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime3); } @@ -752,44 +752,44 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) } const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime }; + ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(releases.moreResults, false); ASSERT_EQ(releases.results.size(), 2); EXPECT_EQ(releases.results[0], release2.getId()); @@ -812,17 +812,17 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(releases.results.size(), 0); } @@ -833,7 +833,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) { auto transaction{ session.createSharedTransaction() }; - auto releases{ Listen::getRecentReleases(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results[0], release.getId()); } @@ -847,18 +847,18 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::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 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(tracks.moreResults, false); ASSERT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results[0], track.getId()); @@ -867,7 +867,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::ListenBrainz, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) }; EXPECT_EQ(tracks.moreResults, false); EXPECT_EQ(tracks.results.size(), 0); } @@ -881,39 +881,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack) { auto transaction{ session.createSharedTransaction() }; - auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; + auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; EXPECT_FALSE(listen); } const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; { auto transaction{ session.createSharedTransaction() }; - auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; + auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime1); } const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 }; + ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 }; { auto transaction{ session.createSharedTransaction() }; - auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; + auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime1); } const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} }; - ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 }; + ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 }; { auto transaction{ session.createSharedTransaction() }; - auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) }; + auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; EXPECT_TRUE(listen); EXPECT_EQ(listen->getDateTime(), dateTime3); } @@ -926,44 +926,44 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi) 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 }; + ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::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) }; + ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) }; { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) }; EXPECT_EQ(tracks.moreResults, false); ASSERT_EQ(tracks.results.size(), 2); EXPECT_EQ(tracks.results[0], track2.getId()); @@ -976,14 +976,14 @@ 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 }; + ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::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()}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(tracks.results.size(), 0); } { @@ -994,7 +994,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) { auto transaction{ session.createSharedTransaction() }; - auto tracks{ Listen::getRecentTracks(session, user->getId(), Scrobbler::Internal, {cluster.getId()}) }; + auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) }; EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results[0], track.getId()); } diff --git a/src/libs/services/database/test/StarredArtist.cpp b/src/libs/services/database/test/StarredArtist.cpp index de2f5511..905fc467 100644 --- a/src/libs/services/database/test/StarredArtist.cpp +++ b/src/libs/services/database/test/StarredArtist.cpp @@ -26,102 +26,100 @@ using ScopedStarredArtist = ScopedEntity; TEST_F(DatabaseFixture, StarredArtist) { - ScopedArtist artist {session, "MyArtist"}; - ScopedUser user {session, "MyUser"}; - ScopedUser user2 {session, "MyUser2"}; + ScopedArtist artist{ session, "MyArtist" }; + ScopedUser user{ session, "MyUser" }; + ScopedUser user2{ session, "MyUser2" }; - { - auto transaction {session.createSharedTransaction()}; + { + 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 starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) }; + EXPECT_FALSE(starredArtist); + EXPECT_EQ(StarredArtist::getCount(session), 0); - auto artists {Artist::find(session, Artist::FindParameters {})}; - EXPECT_EQ(artists.results.size(), 1); - } + 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()}; + ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::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 gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) }; + EXPECT_EQ(gotArtist->getId(), starredArtist->getId()); + EXPECT_EQ(StarredArtist::getCount(session), 1); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {Artist::find(session, Artist::FindParameters {})}; - EXPECT_EQ(artists.results.size(), 1); + 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(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(artists.results.size(), 1); - artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); - EXPECT_EQ(artists.results.size(), 0); - } + artists = Artist::find(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(artists.results.size(), 0); + } } TEST_F(DatabaseFixture, StarredArtist_PendingDestroy) { - ScopedArtist artist {session, "MyArtist"}; - ScopedUser user {session, "MyUser"}; - ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedArtist artist{ session, "MyArtist" }; + ScopedUser user{ session, "MyUser" }; + ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(artists.results.size(), 1); + auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; + EXPECT_EQ(artists.results.size(), 1); - starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); - artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); - EXPECT_EQ(artists.results.size(), 0); - } + starredArtist.get().modify()->setSyncState(SyncState::PendingRemove); + artists = Artist::find(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(artists.results.size(), 0); + } } TEST_F(DatabaseFixture, StarredArtist_dateTime) { - ScopedArtist artist1 {session, "MyArtist1"}; - ScopedArtist artist2 {session, "MyArtist2"}; - ScopedUser user {session, "MyUser"}; + 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}; + ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; + ScopedStarredArtist starredArtist2{ session, artist2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; - const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(artists.results.size(), 2); - } + auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; + EXPECT_EQ(artists.results.size(), 2); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - starredArtist1.get().modify()->setDateTime(dateTime); - starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1)); + 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()}; + auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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)); + 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()); - } + auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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 index b60b149f..ba0e5cb5 100644 --- a/src/libs/services/database/test/StarredRelease.cpp +++ b/src/libs/services/database/test/StarredRelease.cpp @@ -26,102 +26,100 @@ using ScopedStarredRelease = ScopedEntity; TEST_F(DatabaseFixture, StarredRelease) { - ScopedRelease release {session, "MyRelease"}; - ScopedUser user {session, "MyUser"}; - ScopedUser user2 {session, "MyUser2"}; + ScopedRelease release{ session, "MyRelease" }; + ScopedUser user{ session, "MyUser" }; + ScopedUser user2{ session, "MyUser2" }; - { - auto transaction {session.createSharedTransaction()}; + { + 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 starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) }; + EXPECT_FALSE(starredRelease); + EXPECT_EQ(StarredRelease::getCount(session), 0); - auto releases {Release::find(session, Release::FindParameters {})}; - EXPECT_EQ(releases.results.size(), 1); - } + 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()}; + ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::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 gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) }; + EXPECT_EQ(gotRelease->getId(), starredRelease->getId()); + EXPECT_EQ(StarredRelease::getCount(session), 1); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto releases {Release::find(session, Release::FindParameters {})}; - EXPECT_EQ(releases.results.size(), 1); + 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(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(releases.results.size(), 1); - releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); - EXPECT_EQ(releases.results.size(), 0); - } + releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(releases.results.size(), 0); + } } TEST_F(DatabaseFixture, Starredrelease_PendingDestroy) { - ScopedRelease release {session, "MyRelease"}; - ScopedUser user {session, "MyUser"}; - ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedRelease release{ session, "MyRelease" }; + ScopedUser user{ session, "MyUser" }; + ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(releases.results.size(), 1); + auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; + EXPECT_EQ(releases.results.size(), 1); - starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); - releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); - EXPECT_EQ(releases.results.size(), 0); - } + starredRelease.get().modify()->setSyncState(SyncState::PendingRemove); + releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(releases.results.size(), 0); + } } TEST_F(DatabaseFixture, StarredRelease_dateTime) { - ScopedRelease release1 {session, "MyRelease1"}; - ScopedRelease release2 {session, "MyRelease2"}; - ScopedUser user {session, "MyUser"}; + 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}; + ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; + ScopedStarredRelease starredRelease2{ session, release2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; - const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} }; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction{ session.createSharedTransaction() }; - auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(releases.results.size(), 2); - } + auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) }; + EXPECT_EQ(releases.results.size(), 2); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction{ session.createUniqueTransaction() }; - starredRelease1.get().modify()->setDateTime(dateTime); - starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1)); + 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()}; + auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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)); + 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()); - } + auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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 index 36c3faac..2c783679 100644 --- a/src/libs/services/database/test/StarredTrack.cpp +++ b/src/libs/services/database/test/StarredTrack.cpp @@ -26,102 +26,100 @@ using ScopedStarredTrack = ScopedEntity; TEST_F(DatabaseFixture, StarredTrack) { - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - ScopedUser user2 {session, "MyUser2"}; + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedUser user2 {session, "MyUser2"}; - { - auto transaction {session.createSharedTransaction()}; + { + 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 starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)}; + EXPECT_FALSE(starredTrack); + EXPECT_EQ(StarredTrack::getCount(session), 0); - auto tracks {Track::find(session, Track::FindParameters {})}; - EXPECT_EQ(tracks.results.size(), 1); - } + 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()}; + ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::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 gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)}; + EXPECT_EQ(gotTrack->getId(), starredTrack->getId()); + EXPECT_EQ(StarredTrack::getCount(session), 1); + } - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {})}; - EXPECT_EQ(tracks.results.size(), 1); + 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(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(tracks.results.size(), 1); - tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal)); - EXPECT_EQ(tracks.results.size(), 0); - } + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(tracks.results.size(), 0); + } } TEST_F(DatabaseFixture, Starredtrack_PendingDestroy) { - ScopedTrack track {session, "MyTrack"}; - ScopedUser user {session, "MyUser"}; - ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal}; + ScopedTrack track {session, "MyTrack"}; + ScopedUser user {session, "MyUser"}; + ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal}; - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction {session.createUniqueTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(tracks.results.size(), 1); + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; + EXPECT_EQ(tracks.results.size(), 1); - starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove); - tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)); - EXPECT_EQ(tracks.results.size(), 0); - } + starredTrack.get().modify()->setSyncState(SyncState::PendingRemove); + tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)); + EXPECT_EQ(tracks.results.size(), 0); + } } TEST_F(DatabaseFixture, StarredTrack_dateTime) { - ScopedTrack track1 {session, "MyTrack1"}; - ScopedTrack track2 {session, "MyTrack2"}; - ScopedUser user {session, "MyUser"}; + 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}; + ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal}; + ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal}; - const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; + const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}}; - { - auto transaction {session.createSharedTransaction()}; + { + auto transaction {session.createSharedTransaction()}; - auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal))}; - EXPECT_EQ(tracks.results.size(), 2); - } + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))}; + EXPECT_EQ(tracks.results.size(), 2); + } - { - auto transaction {session.createUniqueTransaction()}; + { + auto transaction {session.createUniqueTransaction()}; - starredTrack1.get().modify()->setDateTime(dateTime); - starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1)); + 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()}; + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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)); + 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()); - } + auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::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/feedback/CMakeLists.txt b/src/libs/services/feedback/CMakeLists.txt new file mode 100644 index 00000000..02e63624 --- /dev/null +++ b/src/libs/services/feedback/CMakeLists.txt @@ -0,0 +1,29 @@ + +add_library(lmsfeedback SHARED + impl/internal/InternalBackend.cpp + impl/listenbrainz/FeedbacksParser.cpp + impl/listenbrainz/FeedbacksSynchronizer.cpp + impl/listenbrainz/FeedbackTypes.cpp + impl/listenbrainz/ListenBrainzBackend.cpp + impl/listenbrainz/Utils.cpp + impl/FeedbackService.cpp + ) + +target_include_directories(lmsfeedback INTERFACE + include + ) + +target_include_directories(lmsfeedback PRIVATE + include + impl + ) + +target_link_libraries(lmsfeedback PRIVATE + lmsutils + ) + +target_link_libraries(lmsfeedback PUBLIC + lmsdatabase + ) + +install(TARGETS lmsfeedback DESTINATION lib) diff --git a/src/libs/services/feedback/impl/FeedbackService.cpp b/src/libs/services/feedback/impl/FeedbackService.cpp new file mode 100644 index 00000000..4a5297ac --- /dev/null +++ b/src/libs/services/feedback/impl/FeedbackService.cpp @@ -0,0 +1,185 @@ +/* + * 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 "FeedbackService.impl.hpp" + +#include "services/database/Artist.hpp" +#include "services/database/Db.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/User.hpp" +#include "utils/Logger.hpp" + +#include "internal/InternalBackend.hpp" +#include "listenbrainz/ListenBrainzBackend.hpp" + +namespace Feedback +{ + std::unique_ptr createFeedbackService(boost::asio::io_context& ioContext, Db& db) + { + return std::make_unique(ioContext, db); + } + + FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db) + : _db{ db } + { + LMS_LOG(SCROBBLING, INFO) << "Starting service..."; + _backends.emplace(Database::FeedbackBackend::Internal, std::make_unique(_db)); + _backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique(ioContext, _db)); + LMS_LOG(SCROBBLING, INFO) << "Service started!"; + } + + FeedbackService::~FeedbackService() + { + LMS_LOG(SCROBBLING, INFO) << "Service stopped!"; + } + + std::optional FeedbackService::getUserFeedbackBackend(UserId userId) + { + std::optional feedbackBackend; + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + if (const User::pointer user{ User::find(session, userId) }) + feedbackBackend = user->getFeedbackBackend(); + + return feedbackBackend; + } + + void FeedbackService::star(UserId userId, ArtistId artistId) + { + star(userId, artistId); + } + + void FeedbackService::unstar(UserId userId, ArtistId artistId) + { + unstar(userId, artistId); + } + + bool FeedbackService::isStarred(UserId userId, ArtistId artistId) + { + return isStarred(userId, artistId); + } + + Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ArtistId artistId) + { + return getStarredDateTime(userId, artistId); + } + + FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, ArtistSortMethod sortMethod, Range range) + { + auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return {}; + + Artist::FindParameters params; + params.setStarringUser(userId, *backend); + 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 FeedbackService::star(UserId userId, ReleaseId releaseId) + { + star(userId, releaseId); + } + + void FeedbackService::unstar(UserId userId, ReleaseId releaseId) + { + unstar(userId, releaseId); + } + + bool FeedbackService::isStarred(UserId userId, ReleaseId releaseId) + { + return isStarred(userId, releaseId); + } + + Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ReleaseId releaseId) + { + return getStarredDateTime(userId, releaseId); + } + + FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector& clusterIds, Range range) + { + auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return {}; + + Release::FindParameters params; + params.setStarringUser(userId, *backend); + params.setClusters(clusterIds); + params.setSortMethod(ReleaseSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + return Release::find(session, params); + } + + void FeedbackService::star(UserId userId, TrackId trackId) + { + star(userId, trackId); + } + + void FeedbackService::unstar(UserId userId, TrackId trackId) + { + unstar(userId, trackId); + } + + bool FeedbackService::isStarred(UserId userId, TrackId trackId) + { + return isStarred(userId, trackId); + } + + Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, TrackId trackId) + { + return getStarredDateTime(userId, trackId); + } + + FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector& clusterIds, Range range) + { + auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return {}; + + Track::FindParameters params; + params.setStarringUser(userId, *backend); + params.setClusters(clusterIds); + params.setSortMethod(TrackSortMethod::StarredDateDesc); + params.setRange(range); + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + return Track::find(session, params); + } +} // ns Feedback + diff --git a/src/libs/services/feedback/impl/FeedbackService.hpp b/src/libs/services/feedback/impl/FeedbackService.hpp new file mode 100644 index 00000000..279a8fa7 --- /dev/null +++ b/src/libs/services/feedback/impl/FeedbackService.hpp @@ -0,0 +1,78 @@ +/* + * 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/feedback/IFeedbackService.hpp" +#include "IFeedbackBackend.hpp" + +namespace Database +{ + class Db; +} + +namespace Feedback +{ + class FeedbackService : public IFeedbackService + { + public: + FeedbackService(boost::asio::io_context& ioContext, Database::Db& db); + ~FeedbackService(); + + private: + FeedbackService(const FeedbackService&) = delete; + FeedbackService& operator=(const FeedbackService&) = delete; + + 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; + Wt::WDateTime getStarredDateTime(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 releasedId) override; + Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) 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; + Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override; + TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + + std::optional getUserFeedbackBackend(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); + template + Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id); + + Database::Db& _db; + std::unordered_map> _backends; + }; + +} // ns Feedback diff --git a/src/libs/services/feedback/impl/FeedbackService.impl.hpp b/src/libs/services/feedback/impl/FeedbackService.impl.hpp new file mode 100644 index 00000000..f16be52c --- /dev/null +++ b/src/libs/services/feedback/impl/FeedbackService.impl.hpp @@ -0,0 +1,113 @@ +/* + * 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 Feedback +{ + using namespace Database; + + template + void FeedbackService::star(UserId userId, ObjIdType objId) + { + const auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return; + + typename StarredObjType::IdType starredObjId; + { + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; + + typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) }; + 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 = session.create(obj, user, *backend); + } + starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); + starredObjId = starredObj->getId(); + } + _backends[*backend]->onStarred(starredObjId); + } + + template + void FeedbackService::unstar(UserId userId, ObjIdType objId) + { + const auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return; + + typename StarredObjType::IdType starredObjId; + { + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) }; + if (!starredObj) + return; + + starredObjId = starredObj->getId(); + } + _backends[*backend]->onUnstarred(starredObjId); + } + + template + bool FeedbackService::isStarred(UserId userId, ObjIdType objId) + { + const auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return false; + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) }; + return starredObj && (starredObj->getSyncState() != SyncState::PendingRemove); + } + + template + Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ObjIdType objId) + { + const auto backend{ getUserFeedbackBackend(userId) }; + if (!backend) + return {}; + + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + + typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) }; + if (starredObj && (starredObj->getSyncState() != SyncState::PendingRemove)) + return starredObj->getDateTime(); + + return {}; + } + +} // ns Feedback \ No newline at end of file diff --git a/src/libs/services/scrobbling/impl/IScrobbler.hpp b/src/libs/services/feedback/impl/IFeedbackBackend.hpp similarity index 64% rename from src/libs/services/scrobbling/impl/IScrobbler.hpp rename to src/libs/services/feedback/impl/IFeedbackBackend.hpp index e98ea1b7..1fe5ab24 100644 --- a/src/libs/services/scrobbling/impl/IScrobbler.hpp +++ b/src/libs/services/feedback/impl/IFeedbackBackend.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Emeric Poupon + * Copyright (C) 2023 Emeric Poupon * * This file is part of LMS. * @@ -19,35 +19,17 @@ #pragma once -#include -#include -#include - #include "services/database/StarredArtistId.hpp" #include "services/database/StarredReleaseId.hpp" #include "services/database/StarredTrackId.hpp" -#include "services/scrobbling/Listen.hpp" -namespace Database +namespace Feedback { - class Session; - class TrackList; - class User; -} - -namespace Scrobbling -{ - class IScrobbler + class IFeedbackBackend { public: - virtual ~IScrobbler() = default; + virtual ~IFeedbackBackend() = 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; - - // Feedbacks virtual void onStarred(Database::StarredArtistId) = 0; virtual void onUnstarred(Database::StarredArtistId) = 0; virtual void onStarred(Database::StarredReleaseId) = 0; @@ -56,7 +38,6 @@ namespace Scrobbling virtual void onUnstarred(Database::StarredTrackId) = 0; }; - std::unique_ptr createScrobbler(std::string_view backendName); - -} // ns Scrobbling + std::unique_ptr createFeedbackBackend(std::string_view backendName); +} // ns Feedback diff --git a/src/libs/services/feedback/impl/internal/InternalBackend.cpp b/src/libs/services/feedback/impl/internal/InternalBackend.cpp new file mode 100644 index 00000000..e73e3d1f --- /dev/null +++ b/src/libs/services/feedback/impl/internal/InternalBackend.cpp @@ -0,0 +1,84 @@ +/* + * 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 "InternalBackend.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/StarredTrack.hpp" + +namespace Feedback +{ + namespace details + { + template + void onStarred(Database::Session& session, typename StarredObjType::IdType id) + { + auto transaction{ session.createUniqueTransaction() }; + + if (auto starredObj{ StarredObjType::find(session, id) }) + starredObj.modify()->setSyncState(Database::SyncState::Synchronized); + } + + template + void onUnstarred(Database::Session& session, typename StarredObjType::IdType id) + { + auto transaction{ session.createUniqueTransaction() }; + + if (auto starredObj{ StarredObjType::find(session, id) }) + starredObj.remove(); + } + } + + InternalBackend::InternalBackend(Database::Db& db) + : _db{ db } + {} + + void InternalBackend::onStarred(Database::StarredArtistId starredArtistId) + { + details::onStarred(_db.getTLSSession(), starredArtistId); + } + + void InternalBackend::onUnstarred(Database::StarredArtistId starredArtistId) + { + details::onUnstarred(_db.getTLSSession(), starredArtistId); + } + + void InternalBackend::onStarred(Database::StarredReleaseId starredReleaseId) + { + details::onStarred(_db.getTLSSession(), starredReleaseId); + } + + void InternalBackend::onUnstarred(Database::StarredReleaseId starredReleaseId) + { + details::onUnstarred(_db.getTLSSession(), starredReleaseId); + } + + void InternalBackend::onStarred(Database::StarredTrackId starredTrackId) + { + details::onStarred(_db.getTLSSession(), starredTrackId); + } + + void InternalBackend::onUnstarred(Database::StarredTrackId starredTrackId) + { + details::onUnstarred(_db.getTLSSession(), starredTrackId); + } +} // Feedback diff --git a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp b/src/libs/services/feedback/impl/internal/InternalBackend.hpp similarity index 74% rename from src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp rename to src/libs/services/feedback/impl/internal/InternalBackend.hpp index ce3fb608..2b5bca01 100644 --- a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.hpp +++ b/src/libs/services/feedback/impl/internal/InternalBackend.hpp @@ -19,26 +19,21 @@ #pragma once -#include "IScrobbler.hpp" +#include "IFeedbackBackend.hpp" namespace Database { class Db; } -namespace Scrobbling +namespace Feedback { - class InternalScrobbler final : public IScrobbler + class InternalBackend final : public IFeedbackBackend { public: - InternalScrobbler(Database::Db& db); + InternalBackend(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; - void onStarred(Database::StarredArtistId) override; void onUnstarred(Database::StarredArtistId) override; void onStarred(Database::StarredReleaseId) override; @@ -48,5 +43,5 @@ namespace Scrobbling Database::Db& _db; }; -} // Scrobbling +} // Feedback diff --git a/src/libs/services/feedback/impl/listenbrainz/Exception.hpp b/src/libs/services/feedback/impl/listenbrainz/Exception.hpp new file mode 100644 index 00000000..45664f92 --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/Exception.hpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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/Exception.hpp" + +namespace Feedback::ListenBrainz +{ + class Exception : public ::Feedback::Exception + { + public: + using ::Feedback::Exception::Exception; + }; +} diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.cpp b/src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.cpp similarity index 93% rename from src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.cpp rename to src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.cpp index 0b4d5168..79c2f531 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.cpp +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.cpp @@ -19,7 +19,7 @@ #include "FeedbackTypes.hpp" -namespace Scrobbling::ListenBrainz +namespace Feedback::ListenBrainz { std::ostream& operator<<(std::ostream& os, const Feedback& feedback) @@ -27,4 +27,4 @@ namespace Scrobbling::ListenBrainz os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast(feedback.score); return os; } -} // Scrobbling::ListenBrainz +} // Feedback::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.hpp b/src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.hpp similarity index 94% rename from src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.hpp rename to src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.hpp index cb45d992..ec05760d 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbackTypes.hpp +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbackTypes.hpp @@ -23,7 +23,7 @@ #include #include "utils/UUID.hpp" -namespace Scrobbling::ListenBrainz +namespace Feedback::ListenBrainz { // See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc enum class FeedbackType @@ -42,4 +42,4 @@ namespace Scrobbling::ListenBrainz std::ostream& operator<<(std::ostream& os, const Feedback& feedback); -} // Scrobbling::ListenBrainz +} // Feedback::ListenBrainz diff --git a/src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.cpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.cpp new file mode 100644 index 00000000..123a0a2e --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 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 "FeedbacksParser.hpp" + +#include +#include +#include +#include + +#include "Exception.hpp" +#include "Utils.hpp" + +namespace Feedback::ListenBrainz +{ + namespace + { + Feedback parseFeedback(const Wt::Json::Object& feedbackObj) + { + const std::optional recordingMBID{ UUID::fromString(static_cast(feedbackObj.get("recording_mbid"))) }; + if (!recordingMBID) + throw Exception{ "MBID not found!" }; + + return Feedback + { + Wt::WDateTime::fromTime_t(static_cast(feedbackObj.get("created"))), + *recordingMBID, + static_cast(static_cast(feedbackObj.get("score"))) + }; + } + } + + FeedbacksParser::Result FeedbacksParser::parse(std::string_view msgBody) + { + Result res; + + try + { + Wt::Json::Object root; + Wt::Json::parse(std::string{ msgBody }, root); + + const Wt::Json::Array& feedbacks = root.get("feedback"); + + LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks"; + + if (feedbacks.empty()) + return res; + + res.feedbackCount = feedbacks.size(); + + for (const Wt::Json::Value& value : feedbacks) + { + try + { + res.feedbacks.push_back(parseFeedback(value)); + } + catch (const Exception& e) + { + LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping"; + } + catch (const Wt::WException& e) + { + LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping"; + } + } + } + catch (const Wt::WException& error) + { + LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what(); + } + + return res; + } +} // Feedback::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.hpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.hpp similarity index 93% rename from src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.hpp rename to src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.hpp index d42098bd..f745ceda 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.hpp +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksParser.hpp @@ -23,7 +23,7 @@ #include "FeedbackTypes.hpp" -namespace Scrobbling::ListenBrainz +namespace Feedback::ListenBrainz { class FeedbacksParser { @@ -37,4 +37,4 @@ namespace Scrobbling::ListenBrainz static Result parse(std::string_view msgBody); }; -} // Scrobbling::ListenBrainz +} // Feedback::ListenBrainz diff --git a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp new file mode 100644 index 00000000..1cd2c64a --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.cpp @@ -0,0 +1,490 @@ +/* + * 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 "FeedbacksSynchronizer.hpp" + +#include +#include +#include +#include +#include + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredTrack.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" +#include "utils/IConfig.hpp" +#include "utils/http/IClient.hpp" +#include "utils/Service.hpp" + +#include "Exception.hpp" +#include "FeedbacksParser.hpp" +#include "Utils.hpp" + +namespace Feedback::ListenBrainz +{ + namespace + { + std::optional parseTotalFeedbackCount(std::string_view msgBody) + { + try + { + Wt::Json::Object root; + Wt::Json::parse(std::string{ msgBody }, root); + + return static_cast(root.get("total_count")); + } + catch (const Wt::WException& e) + { + LOG(ERROR) << "Cannot parse listen count response: " << e.what(); + return std::nullopt; + } + } + } + + FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client) + : _ioContext{ ioContext } + , _db{ db } + , _client{ client } + , _maxSyncFeedbackCount{ Service::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) } + , _syncFeedbacksPeriod{ Service::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) } + { + LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours"; + + scheduleSync(std::chrono::seconds{ 30 }); + } + + void FeedbacksSynchronizer::enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId) + { + try + { + Database::Session& session{ _db.getTLSSession() }; + + auto transaction{ session.createUniqueTransaction() }; + + Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) }; + if (!starredTrack) + return; + + std::optional recordingMBID{ starredTrack->getTrack()->getRecordingMBID() }; + + switch (type) + { + case FeedbackType::Love: + if (starredTrack->getSyncState() != Database::SyncState::PendingAdd) + starredTrack.modify()->setSyncState(Database::SyncState::PendingAdd); + break; + + case FeedbackType::Erase: + if (!recordingMBID) + { + LOG(DEBUG) << "Track has no recording MBID: erasing star"; + starredTrack.remove(); + } + else + { + // Send the erase order even if it is not on the remote LB server (it may be + // queued for add, or not) + starredTrack.modify()->setSyncState(Database::SyncState::PendingRemove); + } + break; + + default: + throw Exception{ "Unhandled feedback type" }; + } + + if (!recordingMBID) + { + LOG(DEBUG) << "Track has no recording MBID: skipping"; + return; + } + + const std::optional listenBrainzToken{ starredTrack->getUser()->getListenBrainzToken() }; + if (!listenBrainzToken) + return; + + Http::ClientPOSTRequestParameters request; + request.relativeUrl = "/1/feedback/recording-feedback"; + request.message.addHeader("Authorization", "Token " + std::string{ listenBrainzToken->getAsString() }); + + Wt::Json::Object root; + root["recording_mbid"] = Wt::Json::Value{ std::string {recordingMBID->getAsString()} }; + root["score"] = Wt::Json::Value{ static_cast(type) }; + + request.message.addBodyText(Wt::Json::serialize(root)); + request.message.addHeader("Content-Type", "application/json"); + + request.onSuccessFunc = [=](std::string_view /*msgBody*/) + { + _strand.dispatch([=] + { + onFeedbackSent(type, starredTrackId); + }); + }; + _client.sendPOSTRequest(std::move(request)); + } + catch (Exception& e) + { + LOG(DEBUG) << "Cannot send feedback: " << e.what(); + } + } + + void FeedbacksSynchronizer::onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId) + { + assert(_strand.running_in_this_thread()); + + Database::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; + + Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) }; + if (!starredTrack) + { + LOG(DEBUG) << "Starred track not found. deleted?"; + return; + } + + UserContext& userContext{ getUserContext(starredTrack->getUser()->getId()) }; + + switch (type) + { + case FeedbackType::Love: + starredTrack.modify()->setSyncState(Database::SyncState::Synchronized); + LOG(DEBUG) << "State set to synchronized"; + + if (userContext.feedbackCount) + { + (*userContext.feedbackCount)++; + LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'"; + } + break; + + case FeedbackType::Erase: + starredTrack.remove(); + LOG(DEBUG) << "Removed starred track"; + + if (userContext.feedbackCount && *userContext.feedbackCount > 0) + { + (*userContext.feedbackCount)--; + LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'"; + } + break; + + default: + throw Exception{ "Unhandled feedback type" }; + } + } + + void FeedbacksSynchronizer::enquePendingFeedbacks() + { + using namespace Database; + + auto processPendingFeedbacks{ [this](SyncState scrobblingState, FeedbackType feedbackType) + { + RangeResults pendingFeedbacks; + + { + Database::Session& session {_db.getTLSSession()}; + + auto transaction {session.createSharedTransaction()}; + + StarredTrack::FindParameters params; + params.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz, scrobblingState) + .setRange(Database::Range {0, 100}); // don't flood too much? + + pendingFeedbacks = StarredTrack::find(session, params); + } + + LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks"; + + for (const StarredTrackId starredTrackId : pendingFeedbacks.results) + enqueFeedback(feedbackType, starredTrackId); + } }; + + processPendingFeedbacks(SyncState::PendingAdd, FeedbackType::Love); + processPendingFeedbacks(SyncState::PendingRemove, FeedbackType::Erase); + } + + FeedbacksSynchronizer::UserContext& FeedbacksSynchronizer::getUserContext(Database::UserId userId) + { + assert(_strand.running_in_this_thread()); + + auto itContext{ _userContexts.find(userId) }; + if (itContext == std::cend(_userContexts)) + { + std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId); + } + + return itContext->second; + } + + bool FeedbacksSynchronizer::isSyncing() const + { + return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) + { + return contextEntry.second.syncing; + }); + } + + void FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow) + { + if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0) + return; + + LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; + _syncTimer.expires_after(fromNow); + _syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec) + { + if (ec == boost::asio::error::operation_aborted) + { + LOG(DEBUG) << "getFeedbacks aborted"; + return; + } + else if (ec) + { + throw Exception{ "GetFeedbacks timer failure: " + std::string {ec.message()} }; + } + + startSync(); + })); + } + + void FeedbacksSynchronizer::startSync() + { + LOG(DEBUG) << "Starting sync!"; + + assert(!isSyncing()); + assert(_strand.running_in_this_thread()); + + enquePendingFeedbacks(); + + Database::RangeResults userIds; + { + Database::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createSharedTransaction() }; + userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz)); + } + + for (const Database::UserId userId : userIds.results) + startSync(getUserContext(userId)); + + if (!isSyncing()) + scheduleSync(_syncFeedbacksPeriod); + } + + void FeedbacksSynchronizer::startSync(UserContext& context) + { + context.syncing = true; + context.listenBrainzUserName = ""; + context.fetchedFeedbackCount = 0; + context.matchedFeedbackCount = 0; + context.importedFeedbackCount = 0; + + enqueValidateToken(context); + } + + void FeedbacksSynchronizer::onSyncEnded(UserContext& context) + { + _strand.dispatch([this, &context] + { + LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount; + context.syncing = false; + + if (!isSyncing()) + scheduleSync(_syncFeedbacksPeriod); + }); + } + + void FeedbacksSynchronizer::enqueValidateToken(UserContext& context) + { + assert(context.listenBrainzUserName.empty()); + + const std::optional listenBrainzToken{ ListenBrainz::Utils::getListenBrainzToken(_db.getTLSSession(), context.userId) }; + if (!listenBrainzToken) + { + onSyncEnded(context); + return; + } + + Http::ClientGETRequestParameters request; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.relativeUrl = "/1/validate-token"; + request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} }; + request.onSuccessFunc = [this, &context](std::string_view msgBody) + { + context.listenBrainzUserName = ListenBrainz::Utils::parseValidateToken(msgBody); + if (context.listenBrainzUserName.empty()) + { + onSyncEnded(context); + return; + } + enqueGetFeedbackCount(context); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void FeedbacksSynchronizer::enqueGetFeedbackCount(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/feedback/user/" + std::string{ context.listenBrainzUserName } + "/get-feedback?score=1&count=0"; + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [this, &context](std::string_view msgBody) + { + std::string msgBodyCopy{ msgBody }; + _strand.dispatch([this, msgBodyCopy, &context] + { + LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'"; + + const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy); + if (totalFeedbackCount) + LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount; + + bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) }; + context.feedbackCount = totalFeedbackCount; + + if (needSync) + enqueGetFeedbacks(context); + else + onSyncEnded(context); + }); + }; + request.onFailureFunc = [this, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + void FeedbacksSynchronizer::enqueGetFeedbacks(UserContext& context) + { + assert(!context.listenBrainzUserName.empty()); + + Http::ClientGETRequestParameters request; + request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount); + request.priority = Http::ClientRequestParameters::Priority::Low; + request.onSuccessFunc = [this, &context](std::string_view msgBody) + { + std::string msgBodyCopy{ msgBody }; + _strand.dispatch([this, msgBodyCopy, &context] + { + const std::size_t fetchedFeedbackCount{ processGetFeedbacks(msgBodyCopy, context) }; + if (fetchedFeedbackCount == 0 // no more thing available on server + || context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time + || context.fetchedFeedbackCount >= _maxSyncFeedbackCount) + { + onSyncEnded(context); + } + else + { + enqueGetFeedbacks(context); + } + }); + }; + request.onFailureFunc = [=, &context] + { + onSyncEnded(context); + }; + + _client.sendGETRequest(std::move(request)); + } + + std::size_t FeedbacksSynchronizer::processGetFeedbacks(std::string_view msgBody, UserContext& context) + { + const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) }; + + LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries"; + context.fetchedFeedbackCount += parseResult.feedbackCount; + + for (const Feedback& feedback : parseResult.feedbacks) + { + tryImportFeedback(feedback, context); + } + + return parseResult.feedbackCount; + } + + void FeedbacksSynchronizer::tryImportFeedback(const Feedback& feedback, UserContext& context) + { + using namespace Database; + + Session& session{ _db.getTLSSession() }; + + bool needImport{}; + TrackId trackId; + + { + auto transaction{ session.createSharedTransaction() }; + const std::vector tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) }; + if (tracks.size() > 1) + { + LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found"; + return; + } + else if (tracks.empty()) + { + LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID"; + return; + } + + trackId = tracks.front()->getId(); + + const StarredTrack::pointer starredTrack{ StarredTrack::find(session, trackId, context.userId, Database::FeedbackBackend::ListenBrainz) }; + needImport = !starredTrack; + + // don't update starred date time + // no need to update state if it was found as not synchronized + // pending remove => will be removed later + // pending add => will be resent later + } + + if (needImport) + { + LOG(DEBUG) << "Importing feedback '" << feedback << "'"; + + auto transaction{ session.createUniqueTransaction() }; + + const Track::pointer track{ Track::find(session, trackId) }; + if (!track) + return; + + const User::pointer user{ User::find(session, context.userId) }; + if (!user) + return; + + StarredTrack::pointer starredTrack{ session.create(track, user, Database::FeedbackBackend::ListenBrainz) }; + starredTrack.modify()->setSyncState(SyncState::Synchronized); + starredTrack.modify()->setDateTime(feedback.created); + + context.importedFeedbackCount++; + } + else + { + LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported"; + context.matchedFeedbackCount++; + } + } +} // namespace Feedback::ListenBrainz diff --git a/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.hpp b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.hpp new file mode 100644 index 00000000..bbc2bfec --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/FeedbacksSynchronizer.hpp @@ -0,0 +1,102 @@ +/* + * 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/database/StarredTrackId.hpp" +#include "services/database/UserId.hpp" + +#include "FeedbackTypes.hpp" + +namespace Database +{ + class Db; +} + +namespace Http +{ + class IClient; +} + +namespace Feedback::ListenBrainz +{ + class FeedbacksSynchronizer + { + public: + FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client); + + void enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId); + + private: + void onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId); + + void enquePendingFeedbacks(); + + struct UserContext + { + UserContext(Database::UserId id) : userId{ id } {} + + UserContext(const UserContext&) = delete; + UserContext& operator=(const UserContext&) = delete; + + const Database::UserId userId; + bool syncing{}; + std::optional feedbackCount{}; + + // resetted at each sync + std::string listenBrainzUserName; // need to be resolved first + + std::size_t currentOffset{}; + std::size_t fetchedFeedbackCount{}; + std::size_t matchedFeedbackCount{}; + std::size_t importedFeedbackCount{}; + }; + + UserContext& getUserContext(Database::UserId userId); + bool isSyncing() const; + void scheduleSync(std::chrono::seconds fromNow); + void startSync(); + void startSync(UserContext& context); + void onSyncEnded(UserContext& context); + void enqueValidateToken(UserContext& context); + void enqueGetFeedbackCount(UserContext& context); + void enqueGetFeedbacks(UserContext& context); + std::size_t processGetFeedbacks(std::string_view body, UserContext& context); + void tryImportFeedback(const Feedback& feedback, UserContext& context); + + boost::asio::io_context& _ioContext; + boost::asio::io_context::strand _strand{ _ioContext }; + Database::Db& _db; + boost::asio::steady_timer _syncTimer{ _ioContext }; + Http::IClient& _client; + + std::unordered_map _userContexts; + + const std::size_t _maxSyncFeedbackCount; + const std::chrono::hours _syncFeedbacksPeriod; + }; +} // Feedback::ListenBrainz + diff --git a/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp new file mode 100644 index 00000000..d75a9072 --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ListenBrainzBackend.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/StarredArtist.hpp" +#include "services/database/StarredRelease.hpp" +#include "services/database/Track.hpp" +#include "utils/IConfig.hpp" +#include "utils/http/IClient.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" +#include "Utils.hpp" + +namespace Feedback::ListenBrainz +{ + namespace details + { + template + void onStarred(Database::Session& session, typename StarredObjType::IdType id) + { + auto transaction{ session.createUniqueTransaction() }; + + if (auto starredObj{ StarredObjType::find(session, id) }) + { + // maybe in the future this will be supported by ListenBrainz so set it to PendingAdd for all types + starredObj.modify()->setSyncState(Database::SyncState::PendingAdd); + } + } + + template + void onUnstarred(Database::Session& session, typename StarredObjType::IdType id) + { + auto transaction{ session.createUniqueTransaction() }; + + if (auto starredObj{ StarredObjType::find(session, id) }) + starredObj.remove(); + } + } + + ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db) + : _ioContext{ ioContext } + , _db{ db } + , _baseAPIUrl{ Service::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org") } + , _client{ Http::createClient(_ioContext, _baseAPIUrl) } + , _feedbacksSynchronizer{ _ioContext, db, *_client } + { + LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'"; + } + + ListenBrainzBackend::~ListenBrainzBackend() + { + LOG(INFO) << "Stopped ListenBrainz feedback backend!"; + } + + void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId) + { + details::onStarred(_db.getTLSSession(), starredArtistId); + } + + void ListenBrainzBackend::onUnstarred(Database::StarredArtistId starredArtistId) + { + details::onUnstarred(_db.getTLSSession(), starredArtistId); + } + + void ListenBrainzBackend::onStarred(Database::StarredReleaseId starredReleaseId) + { + details::onStarred(_db.getTLSSession(), starredReleaseId); + } + + void ListenBrainzBackend::onUnstarred(Database::StarredReleaseId starredReleaseId) + { + details::onUnstarred(_db.getTLSSession(), starredReleaseId); + } + + void ListenBrainzBackend::onStarred(Database::StarredTrackId starredTrackId) + { + _feedbacksSynchronizer.enqueFeedback(FeedbackType::Love, starredTrackId); + } + + void ListenBrainzBackend::onUnstarred(Database::StarredTrackId starredtrackId) + { + _feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId); + } +} // namespace Scrobbling::ListenBrainz diff --git a/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.hpp b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.hpp new file mode 100644 index 00000000..4f177b64 --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/ListenBrainzBackend.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "IFeedbackBackend.hpp" +#include "FeedbacksSynchronizer.hpp" + +namespace Database +{ + class Db; +} + +namespace Feedback::ListenBrainz +{ + class ListenBrainzBackend final : public IFeedbackBackend + { + public: + ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db); + ~ListenBrainzBackend() override; + + private: + ListenBrainzBackend(const ListenBrainzBackend&) = delete; + ListenBrainzBackend& operator=(const ListenBrainzBackend&) = delete; + + void onStarred(Database::StarredArtistId starredArtistId) override; + void onUnstarred(Database::StarredArtistId starredArtistId) override; + void onStarred(Database::StarredReleaseId starredReleaseId) override; + void onUnstarred(Database::StarredReleaseId starredReleaseId) override; + void onStarred(Database::StarredTrackId starredTrackId) override; + void onUnstarred(Database::StarredTrackId starredTrackId) override; + + boost::asio::io_context& _ioContext; + Database::Db& _db; + std::string _baseAPIUrl; + std::unique_ptr _client; + FeedbacksSynchronizer _feedbacksSynchronizer; + }; +} // Feedback::ListenBrainz + diff --git a/src/libs/services/feedback/impl/listenbrainz/Utils.cpp b/src/libs/services/feedback/impl/listenbrainz/Utils.cpp new file mode 100644 index 00000000..28b4de9d --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/Utils.cpp @@ -0,0 +1,62 @@ +/* + * 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 "Utils.hpp" + +#include +#include + +#include "services/database/Session.hpp" +#include "services/database/User.hpp" + +namespace Feedback::ListenBrainz::Utils +{ + std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId) + { + auto transaction{ session.createSharedTransaction() }; + + const Database::User::pointer user{ Database::User::find(session, userId) }; + if (!user) + return std::nullopt; + + return user->getListenBrainzToken(); + } + + std::string parseValidateToken(std::string_view msgBody) + { + std::string listenBrainzUserName; + + Wt::Json::ParseError error; + Wt::Json::Object root; + if (!Wt::Json::parse(std::string{ msgBody }, root, error)) + { + LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what(); + return listenBrainzUserName; + } + + if (!root.get("valid").orIfNull(false)) + { + LOG(INFO) << "Invalid listenbrainz user"; + return listenBrainzUserName; + } + + listenBrainzUserName = root.get("user_name").orIfNull(""); + return listenBrainzUserName; + } +} diff --git a/src/libs/services/feedback/impl/listenbrainz/Utils.hpp b/src/libs/services/feedback/impl/listenbrainz/Utils.hpp new file mode 100644 index 00000000..fcbd3d9c --- /dev/null +++ b/src/libs/services/feedback/impl/listenbrainz/Utils.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "services/database/UserId.hpp" +#include "utils/Logger.hpp" +#include "utils/UUID.hpp" + +#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] " + +namespace Database +{ + class Session; +} + +namespace Feedback::ListenBrainz::Utils +{ + std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); + std::string parseValidateToken(std::string_view msgBody); +} diff --git a/src/libs/services/feedback/include/services/feedback/Exception.hpp b/src/libs/services/feedback/include/services/feedback/Exception.hpp new file mode 100644 index 00000000..1a8414fb --- /dev/null +++ b/src/libs/services/feedback/include/services/feedback/Exception.hpp @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#pragma once + +#include "utils/Exception.hpp" + +namespace Feedback +{ + class Exception : public LmsException + { + public: + using LmsException::LmsException; + }; +} diff --git a/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp new file mode 100644 index 00000000..dbd8085f --- /dev/null +++ b/src/libs/services/feedback/include/services/feedback/IFeedbackService.hpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "services/database/Types.hpp" +#include "services/database/ArtistId.hpp" +#include "services/database/ClusterId.hpp" +#include "services/database/ReleaseId.hpp" +#include "services/database/TrackId.hpp" +#include "services/database/UserId.hpp" +#include "services/database/Types.hpp" + +namespace Database +{ + class Db; +} + +namespace Feedback +{ + class IFeedbackService + { + public: + virtual ~IFeedbackService() = default; + + using ArtistContainer = Database::RangeResults; + using ReleaseContainer = Database::RangeResults; + using TrackContainer = Database::RangeResults; + + 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 Wt::WDateTime getStarredDateTime(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 Wt::WDateTime getStarredDateTime(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 Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0; + virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + }; + + std::unique_ptr createFeedbackService(boost::asio::io_service& ioService, Database::Db& db); + +} // ns Feedback + diff --git a/src/libs/services/scrobbling/CMakeLists.txt b/src/libs/services/scrobbling/CMakeLists.txt index 087a3236..2f78000a 100644 --- a/src/libs/services/scrobbling/CMakeLists.txt +++ b/src/libs/services/scrobbling/CMakeLists.txt @@ -1,10 +1,7 @@ add_library(lmsscrobbling SHARED - impl/internal/InternalScrobbler.cpp - impl/listenbrainz/FeedbacksParser.cpp - impl/listenbrainz/FeedbacksSynchronizer.cpp - impl/listenbrainz/FeedbackTypes.cpp - impl/listenbrainz/ListenBrainzScrobbler.cpp + impl/internal/InternalBackend.cpp + impl/listenbrainz/ListenBrainzBackend.cpp impl/listenbrainz/ListenTypes.cpp impl/listenbrainz/ListensParser.cpp impl/listenbrainz/ListensSynchronizer.cpp diff --git a/src/libs/services/scrobbling/impl/IScrobblingBackend.hpp b/src/libs/services/scrobbling/impl/IScrobblingBackend.hpp new file mode 100644 index 00000000..e8db366d --- /dev/null +++ b/src/libs/services/scrobbling/impl/IScrobblingBackend.hpp @@ -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 . + */ + +#pragma once + +#include +#include +#include + +#include "services/scrobbling/Listen.hpp" + +namespace Database +{ + class Session; + class TrackList; + class User; +} + +namespace Scrobbling +{ + class IScrobblingBackend + { + public: + virtual ~IScrobblingBackend() = 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; + }; +} // ns Scrobbling + diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index af331af5..b28b64e2 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -18,22 +18,18 @@ */ #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/User.hpp" #include "utils/Logger.hpp" -#include "internal/InternalScrobbler.hpp" -#include "listenbrainz/ListenBrainzScrobbler.hpp" +#include "internal/InternalBackend.hpp" +#include "listenbrainz/ListenBrainzBackend.hpp" namespace Scrobbling { @@ -48,8 +44,8 @@ namespace Scrobbling : _db{ db } { LMS_LOG(SCROBBLING, INFO) << "Starting service..."; - _scrobblers.emplace(Scrobbler::Internal, std::make_unique(_db)); - _scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique(ioContext, _db)); + _scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique(_db)); + _scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique(ioContext, _db)); LMS_LOG(SCROBBLING, INFO) << "Service started!"; } @@ -60,46 +56,46 @@ namespace Scrobbling void ScrobblingService::listenStarted(const Listen& listen) { - if (std::optional scrobbler{ getUserScrobbler(listen.userId) }) - _scrobblers[*scrobbler]->listenStarted(listen); + if (std::optional backend{ getUserBackend(listen.userId) }) + _scrobblingBackends[*backend]->listenStarted(listen); } void ScrobblingService::listenFinished(const Listen& listen, std::optional duration) { - if (std::optional scrobbler{ getUserScrobbler(listen.userId) }) - _scrobblers[*scrobbler]->listenFinished(listen, duration); + if (std::optional backend{ getUserBackend(listen.userId) }) + _scrobblingBackends[*backend]->listenFinished(listen, duration); } void ScrobblingService::addTimedListen(const TimedListen& listen) { - if (std::optional scrobbler{ getUserScrobbler(listen.userId) }) - _scrobblers[*scrobbler]->addTimedListen(listen); + if (std::optional backend{ getUserBackend(listen.userId) }) + _scrobblingBackends[*backend]->addTimedListen(listen); } - std::optional ScrobblingService::getUserScrobbler(UserId userId) + std::optional ScrobblingService::getUserBackend(UserId userId) { - std::optional scrobbler; + std::optional backend; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; if (const User::pointer user{ User::find(session, userId) }) - scrobbler = user->getScrobbler(); + backend = user->getScrobblingBackend(); - return scrobbler; + return backend; } ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector& clusterIds, std::optional linkType, Range range) { ArtistContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range); + res = Database::Listen::getRecentArtists(session, userId, *backend, clusterIds, linkType, range); return res; } @@ -107,14 +103,14 @@ namespace Scrobbling { ReleaseContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range); + res = Database::Listen::getRecentReleases(session, userId, *backend, clusterIds, range); return res; } @@ -122,40 +118,40 @@ namespace Scrobbling { TrackContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range); + res = Database::Listen::getRecentTracks(session, userId, *backend, clusterIds, range); return res; } Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) { - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return {}; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, releaseId) }; + const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *backend, releaseId) }; return listen ? listen->getDateTime() : Wt::WDateTime{}; } Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) { - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return {}; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, trackId) }; + const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *backend, trackId) }; return listen ? listen->getDateTime() : Wt::WDateTime{}; } @@ -164,14 +160,14 @@ namespace Scrobbling { ArtistContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range); + res = Database::Listen::getTopArtists(session, userId, *backend, clusterIds, linkType, range); return res; } @@ -179,14 +175,14 @@ namespace Scrobbling { ReleaseContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range); + res = Database::Listen::getTopReleases(session, userId, *backend, clusterIds, range); return res; } @@ -194,133 +190,15 @@ namespace Scrobbling { TrackContainer res; - auto scrobbler{ getUserScrobbler(userId) }; - if (!scrobbler) + const auto backend{ getUserBackend(userId) }; + if (!backend) return res; Session& session{ _db.getTLSSession() }; auto transaction{ session.createSharedTransaction() }; - res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range); + res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range); return res; } - - void ScrobblingService::star(UserId userId, ArtistId artistId) - { - star(userId, artistId); - } - - void ScrobblingService::unstar(UserId userId, ArtistId artistId) - { - unstar(userId, artistId); - } - - bool ScrobblingService::isStarred(UserId userId, ArtistId artistId) - { - return isStarred(userId, artistId); - } - - Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId) - { - return getStarredDateTime(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); - } - - Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId) - { - return getStarredDateTime(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); - } - - Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId) - { - return getStarredDateTime(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 68cc27ec..53926120 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.hpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.hpp @@ -24,7 +24,7 @@ #include #include "services/scrobbling/IScrobblingService.hpp" -#include "IScrobbler.hpp" +#include "IScrobblingBackend.hpp" namespace Scrobbling { @@ -39,70 +39,21 @@ namespace Scrobbling void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; - ArtistContainer getRecentArtists(Database::UserId userId, - const std::vector& clusterIds, - std::optional linkType, - Database::Range range) override; - - ReleaseContainer getRecentReleases(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) override; - - TrackContainer getRecentTracks(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) override; + ArtistContainer getRecentArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType,Database::Range range) override; + ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector& clusterIds,Database::Range range) override; + TrackContainer getRecentTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override; Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override; - ArtistContainer getTopArtists(Database::UserId userId, - const std::vector& clusterIds, - std::optional linkType, - Database::Range range) override; + ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::Range range) override; + ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + TrackContainer getTopTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; - ReleaseContainer getTopReleases(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) override; - - TrackContainer getTopTracks(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) override; - - 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; - Wt::WDateTime getStarredDateTime(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 releasedId) override; - Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) 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; - Wt::WDateTime getStarredDateTime(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); - template - Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id); + std::optional getUserBackend(Database::UserId userId); Database::Db& _db; - std::unordered_map> _scrobblers; + std::unordered_map> _scrobblingBackends; }; } // ns Scrobbling diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp deleted file mode 100644 index bf26979b..00000000 --- a/src/libs/services/scrobbling/impl/ScrobblingService.impl.hpp +++ /dev/null @@ -1,114 +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/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; - - typename StarredObjType::IdType starredObjId; - { - 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 = session.create(obj, user, *scrobbler); - } - starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); - starredObjId = starredObj->getId(); - } - _scrobblers[*scrobbler]->onStarred(starredObjId); - } - - template - void ScrobblingService::unstar(UserId userId, ObjIdType objId) - { - auto scrobbler {getUserScrobbler(userId)}; - if (!scrobbler) - return; - - typename StarredObjType::IdType starredObjId; - { - Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - - typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; - if (!starredObj) - return; - - starredObjId = starredObj->getId(); - } - _scrobblers[*scrobbler]->onUnstarred(starredObjId); - } - - template - bool ScrobblingService::isStarred(UserId userId, ObjIdType objId) - { - auto scrobbler {getUserScrobbler(userId)}; - if (!scrobbler) - return false; - - Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - - typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; - return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove); - } - - template - Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ObjIdType objId) - { - auto scrobbler {getUserScrobbler(userId)}; - if (!scrobbler) - return {}; - - Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - - typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; - if (starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove)) - return starredObj->getDateTime(); - - return {}; - } - -} // ns Scrobbling - diff --git a/src/libs/services/scrobbling/impl/internal/InternalBackend.cpp b/src/libs/services/scrobbling/impl/internal/InternalBackend.cpp new file mode 100644 index 00000000..78c4353d --- /dev/null +++ b/src/libs/services/scrobbling/impl/internal/InternalBackend.cpp @@ -0,0 +1,68 @@ +/* + * 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 "InternalBackend.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Listen.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "services/database/User.hpp" + +namespace Scrobbling +{ + InternalBackend::InternalBackend(Database::Db& db) + : _db{ db } + {} + + void InternalBackend::listenStarted(const Listen&) + { + // nothing to do + } + + void InternalBackend::listenFinished(const Listen& listen, std::optional duration) + { + // only record tracks that have been played for at least of few seconds... + if (duration && *duration < std::chrono::seconds{ 5 }) + return; + + addTimedListen({ listen, Wt::WDateTime::currentDateTime() }); + } + + void InternalBackend::addTimedListen(const TimedListen& listen) + { + Database::Session& session{ _db.getTLSSession() }; + auto transaction{ session.createUniqueTransaction() }; + + if (Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::Internal, listen.listenedAt)) + return; + + const Database::User::pointer user{ Database::User::find(session, listen.userId) }; + if (!user) + return; + + const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) }; + if (!track) + return; + + auto dbListen{ session.create(user, track, Database::ScrobblingBackend::Internal, listen.listenedAt) }; + dbListen.modify()->setSyncState(Database::SyncState::Synchronized); + } +} // Scrobbling + diff --git a/src/libs/services/scrobbling/impl/internal/InternalBackend.hpp b/src/libs/services/scrobbling/impl/internal/InternalBackend.hpp new file mode 100644 index 00000000..bd3a3c17 --- /dev/null +++ b/src/libs/services/scrobbling/impl/internal/InternalBackend.hpp @@ -0,0 +1,44 @@ +/* + * 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 "IScrobblingBackend.hpp" + +namespace Database +{ + class Db; +} + +namespace Scrobbling +{ + class InternalBackend final : public IScrobblingBackend + { + public: + InternalBackend(Database::Db& db); + + private: + void listenStarted(const Listen& listen) override; + void listenFinished(const Listen& listen, std::optional duration) override; + void addTimedListen(const TimedListen& listen) override; + + Database::Db& _db; + }; +} // Scrobbling + diff --git a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp b/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp deleted file mode 100644 index 02c9ed1c..00000000 --- a/src/libs/services/scrobbling/impl/internal/InternalScrobbler.cpp +++ /dev/null @@ -1,122 +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 "InternalScrobbler.hpp" - -#include "services/database/Db.hpp" -#include "services/database/Listen.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/User.hpp" - -namespace -{ - template - void onStarred(Database::Session& session, typename StarredObjType::IdType id) - { - auto transaction{ session.createUniqueTransaction() }; - - if (auto starredObj{ StarredObjType::find(session, id) }) - starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); - } - - template - void onUnstarred(Database::Session& session, typename StarredObjType::IdType id) - { - auto transaction{ session.createUniqueTransaction() }; - - if (auto starredObj{ StarredObjType::find(session, id) }) - starredObj.remove(); - } -} - -namespace Scrobbling -{ - InternalScrobbler::InternalScrobbler(Database::Db& db) - : _db{ db } - {} - - void InternalScrobbler::listenStarted(const Listen&) - { - // nothing to do - } - - void InternalScrobbler::listenFinished(const Listen& listen, std::optional duration) - { - // only record tracks that have been played for at least of few seconds... - if (duration && *duration < std::chrono::seconds{ 5 }) - return; - - addTimedListen({ listen, Wt::WDateTime::currentDateTime() }); - } - - void InternalScrobbler::addTimedListen(const TimedListen& listen) - { - Database::Session& session{ _db.getTLSSession() }; - auto transaction{ session.createUniqueTransaction() }; - - 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; - - const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) }; - if (!track) - return; - - auto dbListen{ session.create(user, track, Database::Scrobbler::Internal, listen.listenedAt) }; - dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); - } - - void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId) - { - ::onStarred(_db.getTLSSession(), starredArtistId); - } - - void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId) - { - ::onUnstarred(_db.getTLSSession(), starredArtistId); - } - - void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId) - { - ::onStarred(_db.getTLSSession(), starredReleaseId); - } - - void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId) - { - ::onUnstarred(_db.getTLSSession(), starredReleaseId); - } - - void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId) - { - ::onStarred(_db.getTLSSession(), starredTrackId); - } - - void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId) - { - ::onUnstarred(_db.getTLSSession(), starredTrackId); - } -} // Scrobbling - diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.cpp b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.cpp deleted file mode 100644 index e3ace6cb..00000000 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksParser.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2022 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 "FeedbacksParser.hpp" - -#include -#include -#include -#include - -#include "services/scrobbling/Exception.hpp" -#include "Exception.hpp" -#include "Utils.hpp" - -namespace Scrobbling::ListenBrainz -{ - namespace - { - Feedback - parseFeedback(const Wt::Json::Object& feedbackObj) - { - const std::optional recordingMBID {UUID::fromString(static_cast(feedbackObj.get("recording_mbid")))}; - if (!recordingMBID) - throw Exception {"MBID not found!"}; - - return Feedback - { - Wt::WDateTime::fromTime_t(static_cast(feedbackObj.get("created"))), - *recordingMBID, - static_cast(static_cast(feedbackObj.get("score"))) - }; - } - } - - FeedbacksParser::Result - FeedbacksParser::parse(std::string_view msgBody) - { - Result res; - - try - { - Wt::Json::Object root; - Wt::Json::parse(std::string {msgBody}, root); - - const Wt::Json::Array& feedbacks = root.get("feedback"); - - LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks"; - - if (feedbacks.empty()) - return res; - - res.feedbackCount = feedbacks.size(); - - for (const Wt::Json::Value& value : feedbacks) - { - try - { - res.feedbacks.push_back(parseFeedback(value)); - } - catch (const Exception& e) - { - LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping"; - } - catch (const Wt::WException &e) - { - LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping"; - } - } - } - catch (const Wt::WException& error) - { - LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what(); - } - - return res; - } -} // Scrobbling::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp deleted file mode 100644 index e9265122..00000000 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.cpp +++ /dev/null @@ -1,505 +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 "ListenBrainzScrobbler.hpp" - -#include -#include -#include -#include -#include -#include - -#include "services/database/Db.hpp" -#include "services/database/Session.hpp" -#include "services/database/StarredTrack.hpp" -#include "services/database/Track.hpp" -#include "services/database/User.hpp" -#include "utils/IConfig.hpp" -#include "utils/http/IClient.hpp" -#include "utils/Service.hpp" - -#include "Exception.hpp" -#include "FeedbacksParser.hpp" -#include "Utils.hpp" - -using namespace Scrobbling::ListenBrainz; -using namespace Database; - -namespace -{ - std::optional - parseTotalFeedbackCount(std::string_view msgBody) - { - try - { - Wt::Json::Object root; - Wt::Json::parse(std::string {msgBody}, root); - - return static_cast(root.get("total_count")); - } - catch (const Wt::WException& e) - { - LOG(ERROR) << "Cannot parse listen count response: " << e.what(); - return std::nullopt; - } - } -} - -namespace Scrobbling::ListenBrainz -{ - FeedbacksSynchronizer::FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client) - : _ioContext {ioContext} - , _db {db} - , _client {client} - , _maxSyncFeedbackCount {Service::get()->getULong("listenbrainz-max-sync-feedback-count", 1000)} - , _syncFeedbacksPeriod {Service::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1)} - { - LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours"; - - scheduleSync(std::chrono::seconds {30}); - } - - void - FeedbacksSynchronizer::enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId) - { - try - { - Database::Session& session {_db.getTLSSession()}; - - auto transaction {session.createUniqueTransaction()}; - - StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)}; - if (!starredTrack) - return; - - std::optional recordingMBID {starredTrack->getTrack()->getRecordingMBID()}; - - switch (type) - { - case FeedbackType::Love: - if (starredTrack->getScrobblingState() != ScrobblingState::PendingAdd) - starredTrack.modify()->setScrobblingState(ScrobblingState::PendingAdd); - break; - - case FeedbackType::Erase: - if (!recordingMBID) - { - LOG(DEBUG) << "Track has no recording MBID: erasing star"; - starredTrack.remove(); - } - else - { - // Send the erase order even if it is not on the remote LB server (it may be - // queued for add, or not) - starredTrack.modify()->setScrobblingState(ScrobblingState::PendingRemove); - } - break; - - default: - throw Exception {"Unhandled feedback type"}; - } - - if (!recordingMBID) - { - LOG(DEBUG) << "Track has no recording MBID: skipping"; - return; - } - - const std::optional listenBrainzToken {starredTrack->getUser()->getListenBrainzToken()}; - if (!listenBrainzToken) - return; - - Http::ClientPOSTRequestParameters request; - request.relativeUrl = "/1/feedback/recording-feedback"; - request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()}); - - Wt::Json::Object root; - root["recording_mbid"] = Wt::Json::Value {std::string {recordingMBID->getAsString()}}; - root["score"] = Wt::Json::Value {static_cast(type)}; - - request.message.addBodyText(Wt::Json::serialize(root)); - request.message.addHeader("Content-Type", "application/json"); - - request.onSuccessFunc = [=](std::string_view /*msgBody*/) - { - _strand.dispatch([=] - { - onFeedbackSent(type, starredTrackId); - }); - }; - _client.sendPOSTRequest(std::move(request)); - } - catch (Exception& e) - { - LOG(DEBUG) << "Cannot send feedback: " << e.what(); - } - } - - void - FeedbacksSynchronizer::onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId) - { - assert(_strand.running_in_this_thread()); - - Session& session {_db.getTLSSession()}; - auto transaction {session.createUniqueTransaction()}; - - StarredTrack::pointer starredTrack {StarredTrack::find(session, starredTrackId)}; - if (!starredTrack) - { - LOG(DEBUG) << "Starred track not found. deleted?"; - return; - } - - UserContext& userContext {getUserContext(starredTrack->getUser()->getId())}; - - switch (type) - { - case FeedbackType::Love: - starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized); - LOG(DEBUG) << "State set to synchronized"; - - if (userContext.feedbackCount) - { - (*userContext.feedbackCount)++; - LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'"; - } - break; - - case FeedbackType::Erase: - starredTrack.remove(); - LOG(DEBUG) << "Removed starred track"; - - if (userContext.feedbackCount && *userContext.feedbackCount > 0) - { - (*userContext.feedbackCount)--; - LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName <<"'"; - } - break; - - default: - throw Exception {"Unhandled feedback type"}; - } - } - - void - FeedbacksSynchronizer::enquePendingFeedbacks() - { - auto processPendingFeedbacks { [this] (ScrobblingState scrobblingState, FeedbackType feedbackType) - { - RangeResults pendingFeedbacks; - - { - Database::Session& session {_db.getTLSSession()}; - - auto transaction {session.createSharedTransaction()}; - - StarredTrack::FindParameters params; - params.setScrobbler(Database::Scrobbler::ListenBrainz, scrobblingState) - .setRange(Database::Range {0, 100}); // don't flood too much? - - pendingFeedbacks = StarredTrack::find(session, params); - } - - LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks"; - - for (const StarredTrackId starredTrackId : pendingFeedbacks.results) - enqueFeedback(feedbackType, starredTrackId); - }}; - - processPendingFeedbacks(ScrobblingState::PendingAdd, FeedbackType::Love); - processPendingFeedbacks(ScrobblingState::PendingRemove, FeedbackType::Erase); - } - - FeedbacksSynchronizer::UserContext& - FeedbacksSynchronizer::getUserContext(Database::UserId userId) - { - assert(_strand.running_in_this_thread()); - - auto itContext {_userContexts.find(userId)}; - if (itContext == std::cend(_userContexts)) - { - std::tie(itContext, std::ignore) = _userContexts.emplace(userId, userId); - } - - return itContext->second; - } - - bool - FeedbacksSynchronizer::isSyncing() const - { - return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) - { - return contextEntry.second.syncing; - }); - } - - void - FeedbacksSynchronizer::scheduleSync(std::chrono::seconds fromNow) - { - if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0) - return; - - LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds..."; - _syncTimer.expires_after(fromNow); - _syncTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec) - { - if (ec == boost::asio::error::operation_aborted) - { - LOG(DEBUG) << "getFeedbacks aborted"; - return; - } - else if (ec) - { - throw Exception {"GetFeedbacks timer failure: " + std::string {ec.message()} }; - } - - startSync(); - })); - } - - void - FeedbacksSynchronizer::startSync() - { - LOG(DEBUG) << "Starting sync!"; - - assert(!isSyncing()); - assert(_strand.running_in_this_thread()); - - enquePendingFeedbacks(); - - Database::RangeResults userIds; - { - Database::Session& session {_db.getTLSSession()}; - auto transaction {session.createSharedTransaction()}; - userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz)); - } - - for (const Database::UserId userId : userIds.results) - startSync(getUserContext(userId)); - - if (!isSyncing()) - scheduleSync(_syncFeedbacksPeriod); - } - - void - FeedbacksSynchronizer::startSync(UserContext& context) - { - context.syncing = true; - context.listenBrainzUserName = ""; - context.fetchedFeedbackCount = 0; - context.matchedFeedbackCount = 0; - context.importedFeedbackCount = 0; - - enqueValidateToken(context); - } - - void - FeedbacksSynchronizer::onSyncEnded(UserContext& context) - { - _strand.dispatch([this, &context] - { - LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount; - context.syncing = false; - - if (!isSyncing()) - scheduleSync(_syncFeedbacksPeriod); - }); - } - - void - FeedbacksSynchronizer::enqueValidateToken(UserContext& context) - { - assert(context.listenBrainzUserName.empty()); - - const std::optional listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), context.userId)}; - if (!listenBrainzToken) - { - onSyncEnded(context); - return; - } - - Http::ClientGETRequestParameters request; - request.priority = Http::ClientRequestParameters::Priority::Low; - request.relativeUrl = "/1/validate-token"; - request.headers = { {"Authorization", "Token " + std::string {listenBrainzToken->getAsString()}} }; - request.onSuccessFunc = [this, &context] (std::string_view msgBody) - { - context.listenBrainzUserName = Utils::parseValidateToken(msgBody); - if (context.listenBrainzUserName.empty()) - { - onSyncEnded(context); - return; - } - enqueGetFeedbackCount(context); - }; - request.onFailureFunc = [this, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - void - FeedbacksSynchronizer::enqueGetFeedbackCount(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - Http::ClientGETRequestParameters request; - request.relativeUrl = "/1/feedback/user/" + std::string {context.listenBrainzUserName} + "/get-feedback?score=1&count=0"; - request.priority = Http::ClientRequestParameters::Priority::Low; - request.onSuccessFunc = [this, &context] (std::string_view msgBody) - { - std::string msgBodyCopy {msgBody}; - _strand.dispatch([this, msgBodyCopy, &context] - { - LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'"; - - const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy); - if (totalFeedbackCount) - LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount; - - bool needSync {totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount)}; - context.feedbackCount = totalFeedbackCount; - - if (needSync) - enqueGetFeedbacks(context); - else - onSyncEnded(context); - }); - }; - request.onFailureFunc = [this, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - void - FeedbacksSynchronizer::enqueGetFeedbacks(UserContext& context) - { - assert(!context.listenBrainzUserName.empty()); - - Http::ClientGETRequestParameters request; - request.relativeUrl = "/1/feedback/user/" + context.listenBrainzUserName + "/get-feedback?offset=" + std::to_string(context.fetchedFeedbackCount); - request.priority = Http::ClientRequestParameters::Priority::Low; - request.onSuccessFunc = [this, &context] (std::string_view msgBody) - { - std::string msgBodyCopy {msgBody}; - _strand.dispatch([this, msgBodyCopy, &context] - { - const std::size_t fetchedFeedbackCount {processGetFeedbacks(msgBodyCopy, context)}; - if (fetchedFeedbackCount == 0 // no more thing available on server - || context.fetchedFeedbackCount >= context.feedbackCount // we may miss something, but we will get it next time - || context.fetchedFeedbackCount >= _maxSyncFeedbackCount) - { - onSyncEnded(context); - } - else - { - enqueGetFeedbacks(context); - } - }); - }; - request.onFailureFunc = [=, &context] - { - onSyncEnded(context); - }; - - _client.sendGETRequest(std::move(request)); - } - - std::size_t - FeedbacksSynchronizer::processGetFeedbacks(std::string_view msgBody, UserContext& context) - { - const FeedbacksParser::Result parseResult {FeedbacksParser::parse(msgBody)}; - - LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries"; - context.fetchedFeedbackCount += parseResult.feedbackCount; - - for (const Feedback& feedback : parseResult.feedbacks) - { - tryImportFeedback(feedback, context); - } - - return parseResult.feedbackCount; - } - - void - FeedbacksSynchronizer::tryImportFeedback(const Feedback& feedback, UserContext& context) - { - Database::Session& session {_db.getTLSSession()}; - - bool needImport{}; - TrackId trackId; - - { - auto transaction {session.createSharedTransaction()}; - const std::vector tracks {Track::findByRecordingMBID(session, feedback.recordingMBID)}; - if (tracks.size() > 1) - { - LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found"; - return; - } - else if (tracks.empty()) - { - LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID"; - return; - } - - trackId = tracks.front()->getId(); - - const StarredTrack::pointer starredTrack {StarredTrack::find(session, trackId, context.userId, Database::Scrobbler::ListenBrainz)}; - needImport = !starredTrack; - - // don't update starred date time - // no need to update state if it was found as not synchronized - // pending remove => will be removed later - // pending add => will be resent later - } - - if (needImport) - { - LOG(DEBUG) << "Importing feedback '" << feedback << "'"; - - auto transaction {session.createUniqueTransaction()}; - - const Track::pointer track {Track::find(session, trackId)}; - if (!track) - return; - - const User::pointer user {User::find(session, context.userId)}; - if (!user) - return; - - StarredTrack::pointer starredTrack {session.create(track, user, Database::Scrobbler::ListenBrainz)}; - starredTrack.modify()->setScrobblingState(ScrobblingState::Synchronized); - starredTrack.modify()->setDateTime(feedback.created); - - context.importedFeedbackCount++; - } - else - { - LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported"; - context.matchedFeedbackCount++; - } - } -} // namespace Scrobbling::ListenBrainz diff --git a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.hpp deleted file mode 100644 index b0115d7d..00000000 --- a/src/libs/services/scrobbling/impl/listenbrainz/FeedbacksSynchronizer.hpp +++ /dev/null @@ -1,106 +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 -#include -#include -#include -#include - -#include "services/database/Types.hpp" -#include "services/database/UserId.hpp" - -#include "services/scrobbling/Listen.hpp" - -#include "FeedbackTypes.hpp" - - -namespace Database -{ - class Db; -} - -namespace Http -{ - class IClient; -} - -namespace Scrobbling::ListenBrainz -{ - class FeedbacksSynchronizer - { - public: - FeedbacksSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client); - - void enqueFeedback(FeedbackType type, Database::StarredTrackId starredTrackId); - - private: - void onFeedbackSent(FeedbackType type, Database::StarredTrackId starredTrackId); - - void enquePendingFeedbacks(); - - 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 syncing {}; - std::optional feedbackCount {}; - - // resetted at each sync - std::string listenBrainzUserName; // need to be resolved first - - std::size_t currentOffset{}; - std::size_t fetchedFeedbackCount{}; - std::size_t matchedFeedbackCount{}; - std::size_t importedFeedbackCount{}; - }; - - UserContext& getUserContext(Database::UserId userId); - bool isSyncing() const; - void scheduleSync(std::chrono::seconds fromNow); - void startSync(); - void startSync(UserContext& context); - void onSyncEnded(UserContext& context); - void enqueValidateToken(UserContext& context); - void enqueGetFeedbackCount(UserContext& context); - void enqueGetFeedbacks(UserContext& context); - std::size_t processGetFeedbacks(std::string_view body, UserContext& context); - void tryImportFeedback(const Feedback& feedback, UserContext& context); - - boost::asio::io_context& _ioContext; - boost::asio::io_context::strand _strand {_ioContext}; - Database::Db& _db; - boost::asio::steady_timer _syncTimer {_ioContext}; - Http::IClient& _client; - - std::unordered_map _userContexts; - - const std::size_t _maxSyncFeedbackCount; - const std::chrono::hours _syncFeedbacksPeriod; - }; -} // Scrobbling::ListenBrainz - diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.cpp new file mode 100644 index 00000000..1b1a4350 --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.cpp @@ -0,0 +1,87 @@ +/* + * 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 "ListenBrainzBackend.hpp" + +#include "services/database/Db.hpp" +#include "services/database/Session.hpp" +#include "services/database/Track.hpp" +#include "utils/IConfig.hpp" +#include "utils/http/IClient.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" +#include "Utils.hpp" + +namespace Scrobbling::ListenBrainz +{ + using namespace Database; + + namespace + { + bool canBeScrobbled(Session& session, TrackId trackId, std::chrono::seconds duration) + { + auto transaction{ session.createSharedTransaction() }; + + const Track::pointer track{ Track::find(session, trackId) }; + if (!track) + return false; + + const bool res{ duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2) }; + if (!res) + LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast(track->getDuration()).count() << "s"; + + return res; + } + } + + ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Db& db) + : _ioContext{ ioContext } + , _db{ db } + , _baseAPIUrl{ Service::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org") } + , _client{ Http::createClient(_ioContext, _baseAPIUrl) } + , _listensSynchronizer{ _ioContext, db, *_client } + { + LOG(INFO) << "Starting ListenBrainz backend... API endpoint = '" << _baseAPIUrl << "'"; + } + + ListenBrainzBackend::~ListenBrainzBackend() + { + LOG(INFO) << "Stopped ListenBrainz backend!"; + } + + void ListenBrainzBackend::listenStarted(const Listen& listen) + { + _listensSynchronizer.enqueListenNow(listen); + } + + void ListenBrainzBackend::listenFinished(const Listen& listen, std::optional duration) + { + if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration)) + return; + + const TimedListen timedListen{ listen, Wt::WDateTime::currentDateTime() }; + _listensSynchronizer.enqueListen(timedListen); + } + + void ListenBrainzBackend::addTimedListen(const TimedListen& timedListen) + { + _listensSynchronizer.enqueListen(timedListen); + } +} // namespace Scrobbling::ListenBrainz + diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.hpp new file mode 100644 index 00000000..887dc84f --- /dev/null +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzBackend.hpp @@ -0,0 +1,60 @@ +/* + * 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 "IScrobblingBackend.hpp" +#include "ListensSynchronizer.hpp" + +namespace Database +{ + class Db; +} + +namespace Scrobbling::ListenBrainz +{ + class ListenBrainzBackend final : public IScrobblingBackend + { + public: + ListenBrainzBackend(boost::asio::io_context& ioContext, Database::Db& db); + ~ListenBrainzBackend() override; + + private: + ListenBrainzBackend(const ListenBrainzBackend&) = delete; + ListenBrainzBackend& operator=(const ListenBrainzBackend&) = delete; + + void listenStarted(const Listen& listen) override; + void listenFinished(const Listen& listen, std::optional duration) override; + void addTimedListen(const TimedListen& listen) override; + + // Submit listens + void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); + + boost::asio::io_context& _ioContext; + Database::Db& _db; + std::string _baseAPIUrl; + std::unique_ptr _client; + ListensSynchronizer _listensSynchronizer; + }; +} // Scrobbling::ListenBrainz + diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp deleted file mode 100644 index 063a9987..00000000 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.cpp +++ /dev/null @@ -1,151 +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 "ListenBrainzScrobbler.hpp" - -#include "services/database/Db.hpp" -#include "services/database/Session.hpp" -#include "services/database/StarredArtist.hpp" -#include "services/database/StarredRelease.hpp" -#include "services/database/Track.hpp" -#include "utils/IConfig.hpp" -#include "utils/http/IClient.hpp" -#include "utils/Logger.hpp" -#include "utils/Service.hpp" -#include "Utils.hpp" - -using namespace Database; - -namespace -{ - bool - canBeScrobbled(Session& session, TrackId trackId, std::chrono::seconds duration) - { - auto transaction {session.createSharedTransaction()}; - - const Track::pointer track {Track::find(session, trackId)}; - if (!track) - return false; - - const bool res {duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2)}; - if (!res) - LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast(track->getDuration()).count() << "s"; - - return res; - } - - template - void onStarred(Database::Session& session, typename StarredObjType::IdType id) - { - auto transaction {session.createUniqueTransaction()}; - - if (auto starredObj {StarredObjType::find(session, id)}) - { - // maybe in the future this will be supported by ListenBrainz so set it to PendingAdd - starredObj.modify()->setScrobblingState(Database::ScrobblingState::PendingAdd); - } - } - - template - void onUnstarred(Database::Session& session, typename StarredObjType::IdType id) - { - auto transaction {session.createUniqueTransaction()}; - - if (auto starredObj {StarredObjType::find(session, id)}) - starredObj.remove(); - } -} - -namespace Scrobbling::ListenBrainz -{ - Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Db& db) - : _ioContext {ioContext} - , _db {db} - , _baseAPIUrl {Service::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")} - , _client {Http::createClient(_ioContext, _baseAPIUrl)} - , _listensSynchronizer {_ioContext, db, *_client} - , _feedbacksSynchronizer {_ioContext, db, *_client} - { - LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _baseAPIUrl << "'"; - } - - Scrobbler::~Scrobbler() - { - LOG(INFO) << "Stopped ListenBrainz scrobbler!"; - } - - void - Scrobbler::listenStarted(const Listen& listen) - { - _listensSynchronizer.enqueListenNow(listen); - } - - void - Scrobbler::listenFinished(const Listen& listen, std::optional duration) - { - if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration)) - return; - - const TimedListen timedListen {listen, Wt::WDateTime::currentDateTime()}; - _listensSynchronizer.enqueListen(timedListen); - } - - void - Scrobbler::addTimedListen(const TimedListen& timedListen) - { - _listensSynchronizer.enqueListen(timedListen); - } - - void - Scrobbler::onStarred(StarredArtistId starredArtistId) - { - ::onStarred(_db.getTLSSession(), starredArtistId); - } - - void - Scrobbler::onUnstarred(StarredArtistId starredArtistId) - { - ::onUnstarred(_db.getTLSSession(), starredArtistId); - } - - void - Scrobbler::onStarred(StarredReleaseId starredReleaseId) - { - ::onStarred(_db.getTLSSession(), starredReleaseId); - } - - void - Scrobbler::onUnstarred(StarredReleaseId starredReleaseId) - { - ::onUnstarred(_db.getTLSSession(), starredReleaseId); - } - - void - Scrobbler::onStarred(StarredTrackId starredTrackId) - { - _feedbacksSynchronizer.enqueFeedback(FeedbackType::Love, starredTrackId); - } - - void - Scrobbler::onUnstarred(StarredTrackId starredtrackId) - { - _feedbacksSynchronizer.enqueFeedback(FeedbackType::Erase, starredtrackId); - } -} // namespace Scrobbling::ListenBrainz - diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp deleted file mode 100644 index e91159f6..00000000 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListenBrainzScrobbler.hpp +++ /dev/null @@ -1,72 +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 -#include -#include - -#include "IScrobbler.hpp" -#include "FeedbacksSynchronizer.hpp" -#include "ListensSynchronizer.hpp" - -namespace Database -{ - class Db; -} - -namespace Scrobbling::ListenBrainz -{ - class Scrobbler final : public IScrobbler - { - public: - Scrobbler(boost::asio::io_context& ioContext, Database::Db& db); - ~Scrobbler(); - - Scrobbler(const Scrobbler&) = delete; - Scrobbler(const Scrobbler&&) = delete; - Scrobbler& operator=(const Scrobbler&) = delete; - Scrobbler& operator=(const Scrobbler&&) = delete; - - private: - void listenStarted(const Listen& listen) override; - void listenFinished(const Listen& listen, std::optional duration) override; - void addTimedListen(const TimedListen& listen) override; - - // Submit listens - void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); - - // Star - void onStarred(Database::StarredArtistId starredArtistId) override; - void onUnstarred(Database::StarredArtistId starredArtistId) override; - void onStarred(Database::StarredReleaseId starredReleaseId) override; - void onUnstarred(Database::StarredReleaseId starredReleaseId) override; - void onStarred(Database::StarredTrackId starredTrackId) override; - void onUnstarred(Database::StarredTrackId starredTrackId) override; - - boost::asio::io_context& _ioContext; - Database::Db& _db; - std::string _baseAPIUrl; - std::unique_ptr _client; - ListensSynchronizer _listensSynchronizer; - FeedbacksSynchronizer _feedbacksSynchronizer; - }; -} // Scrobbling::ListenBrainz - diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp index caf42117..84cd6b85 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.cpp @@ -17,9 +17,8 @@ * along with LMS. If not, see . */ -#include "ListenBrainzScrobbler.hpp" +#include "ListensSynchronizer.hpp" -#include #include #include #include @@ -248,14 +247,14 @@ namespace Scrobbling::ListenBrainz { const TimedListen timedListen {listen, timePoint}; // We want the listen to be sent again later in case of failure, so we just save it as pending send - saveListen(timedListen, Database::ScrobblingState::PendingAdd); + saveListen(timedListen, Database::SyncState::PendingAdd); request.priority = Http::ClientRequestParameters::Priority::Normal; request.onSuccessFunc = [=](std::string_view) { _strand.dispatch([=] { - if (saveListen(timedListen, Database::ScrobblingState::Synchronized)) + if (saveListen(timedListen, Database::SyncState::Synchronized)) { UserContext& context {getUserContext(listen.userId)}; if (context.listenCount) @@ -293,14 +292,14 @@ namespace Scrobbling::ListenBrainz } bool - ListensSynchronizer::saveListen(const TimedListen& listen, Database::ScrobblingState scrobblingState) + ListensSynchronizer::saveListen(const TimedListen& listen, Database::SyncState scrobblingState) { using namespace Database; Session& session {_db.getTLSSession()}; auto transaction {session.createUniqueTransaction()}; // TODO: unique only if needed - Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt)}; + Database::Listen::pointer dbListen {Database::Listen::find(session, listen.userId, listen.trackId, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt)}; if (!dbListen) { const User::pointer user {User::find(session, listen.userId)}; @@ -311,18 +310,18 @@ namespace Scrobbling::ListenBrainz if (!track) return false; - dbListen = session.create(user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt); - dbListen.modify()->setScrobblingState(scrobblingState); + dbListen = session.create(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt); + dbListen.modify()->setSyncState(scrobblingState); LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString(); return true; } - if (dbListen->getScrobblingState() == scrobblingState) + if (dbListen->getSyncState() == scrobblingState) return false; - dbListen.modify()->setScrobblingState(scrobblingState); + dbListen.modify()->setSyncState(scrobblingState); return true; } @@ -337,8 +336,8 @@ namespace Scrobbling::ListenBrainz auto transaction {session.createUniqueTransaction()}; Database::Listen::FindParameters params; - params.setScrobbler(Database::Scrobbler::ListenBrainz) - .setScrobblingState(Database::ScrobblingState::PendingAdd) + params.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz) + .setSyncState(Database::SyncState::PendingAdd) .setRange(Database::Range {0, 100}); // don't flood too much? const Database::RangeResults results {Database::Listen::find(session, params)}; @@ -423,7 +422,7 @@ namespace Scrobbling::ListenBrainz { Database::Session& session {_db.getTLSSession()}; auto transaction {session.createSharedTransaction()}; - userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobbler(Database::Scrobbler::ListenBrainz)); + userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz)); } for (const Database::UserId userId : userIds.results) @@ -583,7 +582,7 @@ namespace Scrobbling::ListenBrainz context.matchedListenCount++; const Scrobbling::TimedListen listen {{context.userId, trackId}, parsedListen.listenedAt}; - if (saveListen(listen, Database::ScrobblingState::Synchronized)) + if (saveListen(listen, Database::SyncState::Synchronized)) context.importedListenCount++; } } diff --git a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp index 0102409e..24686076 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/ListensSynchronizer.hpp @@ -52,7 +52,7 @@ namespace Scrobbling::ListenBrainz private: void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint); - bool saveListen(const TimedListen& listen, Database::ScrobblingState scrobblinState); + bool saveListen(const TimedListen& listen, Database::SyncState scrobblinState); void enquePendingListens(); diff --git a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp index f3454f40..a02eaca7 100644 --- a/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp +++ b/src/libs/services/scrobbling/impl/listenbrainz/Utils.hpp @@ -19,12 +19,11 @@ #pragma once -#include "utils/UUID.hpp" - #include "services/database/UserId.hpp" #include "utils/Logger.hpp" +#include "utils/UUID.hpp" -#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - " +#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] " namespace Database { @@ -34,6 +33,5 @@ namespace Database namespace Scrobbling::ListenBrainz::Utils { std::optional getListenBrainzToken(Database::Session& session, Database::UserId userId); - std::string parseValidateToken(std::string_view msgBody); } diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index 507f5ad4..fcabbdb6 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -55,59 +55,19 @@ namespace Scrobbling 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, - Database::Range range) = 0; - - virtual ReleaseContainer getRecentReleases(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) = 0; - - virtual TrackContainer getRecentTracks(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) = 0; + virtual ArtistContainer getRecentArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::Range range) = 0; + virtual ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual TrackContainer getRecentTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0; // Top - virtual ArtistContainer getTopArtists(Database::UserId userId, - const std::vector& clusterIds, - std::optional linkType, - Database::Range) = 0; - - virtual ReleaseContainer getTopReleases(Database::UserId userId, - const std::vector& clusterIds, - Database::Range range) = 0; - - virtual TrackContainer getTopTracks(Database::UserId userId, - const std::vector& clusterIds, - 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 Wt::WDateTime getStarredDateTime(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 Wt::WDateTime getStarredDateTime(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 Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0; - virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::Range) = 0; + virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual TrackContainer getTopTracks(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/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 8964a561..2ba6eda8 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -42,6 +42,7 @@ target_link_libraries(lmssubsonic PRIVATE lmsauth lmsav lmsdatabase + lmsfeedback lmsrecommendation lmsscanner lmsscrobbling diff --git a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp index 2cb08ca0..f245287f 100644 --- a/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/entrypoints/AlbumSongLists.cpp @@ -25,6 +25,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 "responses/Album.hpp" #include "responses/Artist.hpp" @@ -49,7 +50,8 @@ namespace API::Subsonic const Range range{ offset, size }; RangeResults releases; - Scrobbling::IScrobblingService& scrobbling{ *Service::get() }; + Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; + Feedback::IFeedbackService& feedbackService{ *Service::get() }; auto transaction{ context.dbSession.createSharedTransaction() }; @@ -101,7 +103,7 @@ namespace API::Subsonic } else if (type == "frequent") { - releases = scrobbling.getTopReleases(context.userId, {}, range); + releases = scrobblingService.getTopReleases(context.userId, {}, range); } else if (type == "newest") { @@ -123,11 +125,11 @@ namespace API::Subsonic } else if (type == "recent") { - releases = scrobbling.getRecentReleases(context.userId, {}, range); + releases = scrobblingService.getRecentReleases(context.userId, {}, range); } else if (type == "starred") { - releases = scrobbling.getStarredReleases(context.userId, {}, range); + releases = feedbackService.getStarredReleases(context.userId, {}, range); } else throw NotImplementedGenericError{}; @@ -155,21 +157,21 @@ namespace API::Subsonic Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) }; - Scrobbling::IScrobblingService& scrobbling{ *Service::get() }; + Feedback::IFeedbackService& feedbackService{ *Service::get() }; - for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) + for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) { if (auto artist{ Artist::find(context.dbSession, artistId) }) starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3)); } - for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) + for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) { if (auto release{ Release::find(context.dbSession, releaseId) }) starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3)); } - for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) + for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) { if (auto track{ Track::find(context.dbSession, trackId) }) starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user)); diff --git a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp index 96aa7c2c..2f7d8d6a 100644 --- a/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp +++ b/src/libs/subsonic/impl/entrypoints/MediaAnnotation.cpp @@ -24,6 +24,7 @@ #include "services/database/ArtistId.hpp" #include "services/database/ReleaseId.hpp" #include "services/database/TrackId.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "ParameterParsing.hpp" @@ -60,13 +61,13 @@ namespace API::Subsonic StarParameters params{ getStarParameters(context.parameters) }; for (const ArtistId id : params.artistIds) - Service::get()->star(context.userId, id); + Service::get()->star(context.userId, id); for (const ReleaseId id : params.releaseIds) - Service::get()->star(context.userId, id); + Service::get()->star(context.userId, id); for (const TrackId id : params.trackIds) - Service::get()->star(context.userId, id); + Service::get()->star(context.userId, id); return Response::createOkResponse(context.serverProtocolVersion); } @@ -76,13 +77,13 @@ namespace API::Subsonic StarParameters params{ getStarParameters(context.parameters) }; for (const ArtistId id : params.artistIds) - Service::get()->unstar(context.userId, id); + Service::get()->unstar(context.userId, id); for (const ReleaseId id : params.releaseIds) - Service::get()->unstar(context.userId, id); + Service::get()->unstar(context.userId, id); for (const TrackId id : params.trackIds) - Service::get()->unstar(context.userId, id); + Service::get()->unstar(context.userId, id); return Response::createOkResponse(context.serverProtocolVersion); } diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 0abb0b88..ea878b0a 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -23,6 +23,7 @@ #include "services/database/Artist.hpp" #include "services/database/Release.hpp" #include "services/database/User.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -132,7 +133,7 @@ namespace API::Subsonic albumNode.setAttribute("genre", clusters.front().front()->getName()); } - if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid()) albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime)); // OpenSubsonic specific fields (must always be set) diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 59505c18..e1f4edb1 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -23,7 +23,7 @@ #include "services/database/Release.hpp" #include "services/database/TrackArtistLink.hpp" #include "services/database/User.hpp" -#include "services/scrobbling/IScrobblingService.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -86,7 +86,7 @@ namespace API::Subsonic artistNode.setAttribute("albumCount", releases.results.size()); } - if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid()) artistNode.setAttribute("starred", StringUtils::toISO8601String(dateTime)); // OpenSubsonic specific fields (must always be set) diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 543737f4..46a42e5c 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -27,6 +27,7 @@ #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" #include "services/database/User.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" @@ -148,7 +149,7 @@ namespace API::Subsonic trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); - if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) + if (const Wt::WDateTime dateTime{ Service::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime)); // Report the first GENRE for this track diff --git a/src/libs/utils/impl/Logger.cpp b/src/libs/utils/impl/Logger.cpp index 57d7a35d..604a2802 100644 --- a/src/libs/utils/impl/Logger.cpp +++ b/src/libs/utils/impl/Logger.cpp @@ -31,6 +31,7 @@ const char* getModuleName(Module mod) case Module::DB: return "DB"; case Module::DBUPDATER: return "DB UPDATER"; case Module::FEATURE: return "FEATURE"; + case Module::FEEDBACK: return "FEEDBACK"; case Module::HTTP: return "HTTP"; case Module::MAIN: return "MAIN"; case Module::METADATA: return "METADATA"; diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index 807ec72b..fe2234ea 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include @@ -33,6 +33,63 @@ namespace StringUtils { + namespace + { + constexpr std::pair jsEscapeChars[] + { + { '\\', "\\\\" }, + { '\n', "\\n" }, + { '\r', "\\r" }, + { '\t', "\\t" }, + { '"', "\\\"" }, + { '\'', "\\\'" }, + }; + + constexpr std::pair jsonEscapeChars[] + { + { '\\', "\\\\" }, + { '\n', "\\n" }, + { '\r', "\\r" }, + { '\t', "\\t" }, + { '"', "\\\"" }, + { '\'', "\\\'" }, + }; + + template + std::string escape(std::string_view str, const std::pair(&charsToEscape)[N]) + { + std::string escaped; + escaped.reserve(str.length()); + + for (const char c : str) + { + auto it{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](const auto& entry) { return entry.first == c; }) }; + if (it == std::cend(charsToEscape)) + { + escaped += c; + continue; + } + + escaped += it->second; + } + + return escaped; + } + + template + void writeEscapedString(std::ostream& os, std::string_view str, const std::pair(&charsToEscape)[N]) + { + for (const char c : str) + { + auto itEntry{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [=](const auto& entry) { return entry.first == c;}) }; + if (itEntry != std::cend(charsToEscape)) + os << itEntry->second; + else + os << c; + } + } + } + bool readList(const std::string& str, const std::string& separators, std::list& results) { std::string curStr; @@ -221,54 +278,22 @@ namespace StringUtils std::string jsEscape(std::string_view str) { - static const std::unordered_map escapeMap - { - { '\\', "\\\\" }, - { '\n', "\\n" }, - { '\r', "\\r" }, - { '\t', "\\t" }, - { '"', "\\\"" }, - { '\'', "\\\'" }, - }; - - std::string escaped; - escaped.reserve(str.length()); - - for (const char c : str) - { - auto it{ escapeMap.find(c) }; - if (it == std::cend(escapeMap)) - { - escaped += c; - continue; - } - - escaped += it->second; - } - - return escaped; + return escape(str, jsEscapeChars); } void writeJSEscapedString(std::ostream& os, std::string_view str) { - static constexpr std::pair charsToEscape[] - { - {'\\', "\\\\" }, - { '\n', "\\n" }, - { '\r', "\\r" }, - { '\t', "\\t" }, - { '"', "\\\"" }, - { '\'', "\\\'" }, - }; + writeEscapedString(os, str, jsEscapeChars); + } + + std::string jsonEscape(std::string_view str) + { + return escape(str, jsonEscapeChars); + } - for (const char c : str) - { - auto itEntry{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [=](const auto& entry) { return entry.first == c;}) }; - if (itEntry != std::cend(charsToEscape)) - os << itEntry->second; - else - os << c; - } + void writeJsonEscapedString(std::ostream& os, std::string_view str) + { + writeEscapedString(os, str, jsonEscapeChars); } std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index ba36f4ea..f7a48916 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -43,6 +43,7 @@ enum class Module DB, DBUPDATER, FEATURE, + FEEDBACK, HTTP, MAIN, METADATA, diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 03709ac1..9cf8970a 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -84,7 +84,9 @@ namespace StringUtils { [[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to); [[nodiscard]] std::string jsEscape(std::string_view str); + [[nodiscard]] std::string jsonEscape(std::string_view str); void writeJSEscapedString(std::ostream& os, std::string_view str); + void writeJsonEscapedString(std::ostream& os, std::string_view str); [[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar); diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 482247fa..b42f9023 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -57,6 +57,7 @@ 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 0dd1fe41..543c2475 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -32,6 +32,7 @@ #include "services/cover/ICoverService.hpp" #include "services/database/Db.hpp" #include "services/database/Session.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IPlaylistGeneratorService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "services/scanner/IScannerService.hpp" @@ -50,84 +51,84 @@ static std::size_t getThreadCount() { - const unsigned long configHttpServerThreadCount {Service::get()->getULong("http-server-thread-count", 0)}; + const unsigned long configHttpServerThreadCount{ Service::get()->getULong("http-server-thread-count", 0) }; - // Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg) - return configHttpServerThreadCount ? configHttpServerThreadCount : std::max(2, std::thread::hardware_concurrency()); + // Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg) + return configHttpServerThreadCount ? configHttpServerThreadCount : std::max(2, std::thread::hardware_concurrency()); } static std::vector generateWtConfig(std::string execPath) { - std::vector args; + std::vector args; - const std::filesystem::path wtConfigPath {Service::get()->getPath("working-dir") / "wt_config.xml"}; - const std::filesystem::path wtLogFilePath {Service::get()->getPath("log-file", "/var/log/lms.log")}; - const std::filesystem::path wtAccessLogFilePath {Service::get()->getPath("access-log-file", "/var/log/lms.access.log")}; - const std::filesystem::path wtResourcesPath {Service::get()->getPath("wt-resources", "/usr/share/Wt/resources")}; + const std::filesystem::path wtConfigPath{ Service::get()->getPath("working-dir") / "wt_config.xml" }; + const std::filesystem::path wtLogFilePath{ Service::get()->getPath("log-file", "/var/log/lms.log") }; + const std::filesystem::path wtAccessLogFilePath{ Service::get()->getPath("access-log-file", "/var/log/lms.access.log") }; + const std::filesystem::path wtResourcesPath{ Service::get()->getPath("wt-resources", "/usr/share/Wt/resources") }; - args.push_back(execPath); - args.push_back("--config=" + wtConfigPath.string()); - args.push_back("--docroot=" + std::string {Service::get()->getString("docroot")}); - args.push_back("--approot=" + std::string {Service::get()->getString("approot")}); - args.push_back("--deploy-path=" + std::string {Service::get()->getString("deploy-path", "/")}); - if (!wtResourcesPath.empty()) - args.push_back("--resources-dir=" + wtResourcesPath.string()); + args.push_back(execPath); + args.push_back("--config=" + wtConfigPath.string()); + args.push_back("--docroot=" + std::string{ Service::get()->getString("docroot") }); + args.push_back("--approot=" + std::string{ Service::get()->getString("approot") }); + args.push_back("--deploy-path=" + std::string{ Service::get()->getString("deploy-path", "/") }); + if (!wtResourcesPath.empty()) + args.push_back("--resources-dir=" + wtResourcesPath.string()); - if (Service::get()->getBool("tls-enable", false)) - { - args.push_back("--https-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); - args.push_back("--https-address=" + std::string {Service::get()->getString("listen-addr", "0.0.0.0")}); - args.push_back("--ssl-certificate=" + std::string {Service::get()->getString("tls-cert")}); - args.push_back("--ssl-private-key=" + std::string {Service::get()->getString("tls-key")}); - args.push_back("--ssl-tmp-dh=" + std::string {Service::get()->getString("tls-dh")}); - } - else - { - args.push_back("--http-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); - args.push_back("--http-address=" + std::string {Service::get()->getString("listen-addr", "0.0.0.0")}); - } + if (Service::get()->getBool("tls-enable", false)) + { + args.push_back("--https-port=" + std::to_string(Service::get()->getULong("listen-port", 5082))); + args.push_back("--https-address=" + std::string{ Service::get()->getString("listen-addr", "0.0.0.0") }); + args.push_back("--ssl-certificate=" + std::string{ Service::get()->getString("tls-cert") }); + args.push_back("--ssl-private-key=" + std::string{ Service::get()->getString("tls-key") }); + args.push_back("--ssl-tmp-dh=" + std::string{ Service::get()->getString("tls-dh") }); + } + else + { + args.push_back("--http-port=" + std::to_string(Service::get()->getULong("listen-port", 5082))); + args.push_back("--http-address=" + std::string{ Service::get()->getString("listen-addr", "0.0.0.0") }); + } - if (!wtAccessLogFilePath.empty()) - args.push_back("--accesslog=" + wtAccessLogFilePath.string()); + if (!wtAccessLogFilePath.empty()) + args.push_back("--accesslog=" + wtAccessLogFilePath.string()); - args.push_back("--threads=" + std::to_string(getThreadCount())); + args.push_back("--threads=" + std::to_string(getThreadCount())); - // Generate the wt_config.xml file - boost::property_tree::ptree pt; + // Generate the wt_config.xml file + boost::property_tree::ptree pt; - pt.put("server.application-settings..location", "*"); - pt.put("server.application-settings.log-file", wtLogFilePath.string()); - pt.put("server.application-settings.log-config", Service::get()->getString("log-config", "* -debug -info:WebRequest")); - pt.put("server.application-settings.behind-reverse-proxy", Service::get()->getBool("behind-reverse-proxy", false)); + pt.put("server.application-settings..location", "*"); + pt.put("server.application-settings.log-file", wtLogFilePath.string()); + pt.put("server.application-settings.log-config", Service::get()->getString("log-config", "* -debug -info:WebRequest")); + pt.put("server.application-settings.behind-reverse-proxy", Service::get()->getBool("behind-reverse-proxy", false)); - { - boost::property_tree::ptree viewport; - viewport.put(".name", "viewport"); - viewport.put(".content", "width=device-width, initial-scale=1, user-scalable=no"); - pt.add_child("server.application-settings.head-matter.meta", viewport); - } - { - boost::property_tree::ptree themeColor; - themeColor.put(".name", "theme-color"); - themeColor.put(".content", "#303030"); - pt.add_child("server.application-settings.head-matter.meta", themeColor); - } + { + boost::property_tree::ptree viewport; + viewport.put(".name", "viewport"); + viewport.put(".content", "width=device-width, initial-scale=1, user-scalable=no"); + pt.add_child("server.application-settings.head-matter.meta", viewport); + } + { + boost::property_tree::ptree themeColor; + themeColor.put(".name", "theme-color"); + themeColor.put(".content", "#303030"); + pt.add_child("server.application-settings.head-matter.meta", themeColor); + } - { - std::ofstream oss {wtConfigPath.string().c_str(), std::ios::out}; - if (!oss) - throw LmsException {"Can't open '" + wtConfigPath.string() + "' for writing!"}; + { + std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out }; + if (!oss) + throw LmsException{ "Can't open '" + wtConfigPath.string() + "' for writing!" }; - boost::property_tree::xml_parser::write_xml(oss, pt); + boost::property_tree::xml_parser::write_xml(oss, pt); - if (!oss) - throw LmsException {"Can't write in file '" + wtConfigPath.string() + "', no space left?"}; - } + if (!oss) + throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" }; + } - return args; + return args; } @@ -135,194 +136,195 @@ static void proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server) { - auto postAll {[](Wt::WServer& server, std::function cb) - { - server.postAll([cb = std::move(cb)] - { - // may be nullptr, see https://redmine.webtoolkit.eu/issues/8202 - if (LmsApp) - cb(); - }); - }}; + auto postAll{ [](Wt::WServer& server, std::function cb) + { + server.postAll([cb = std::move(cb)] + { + // may be nullptr, see https://redmine.webtoolkit.eu/issues/8202 + if (LmsApp) + cb(); + }); + } }; - scanner.getEvents().scanStarted.connect([&] - { - postAll(server, [] - { - LmsApp->getScannerEvents().scanStarted.emit(); - LmsApp->triggerUpdate(); - }); - }); + scanner.getEvents().scanStarted.connect([&] + { + postAll(server, [] + { + LmsApp->getScannerEvents().scanStarted.emit(); + LmsApp->triggerUpdate(); + }); + }); - scanner.getEvents().scanComplete.connect([&] (const Scanner::ScanStats& stats) - { - postAll(server, [=] - { - LmsApp->getScannerEvents().scanComplete.emit(stats); - LmsApp->triggerUpdate(); - }); - }); + scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats) + { + postAll(server, [=] + { + LmsApp->getScannerEvents().scanComplete.emit(stats); + LmsApp->triggerUpdate(); + }); + }); - scanner.getEvents().scanInProgress.connect([&] (const Scanner::ScanStepStats& stats) - { - postAll(server, [=] - { - LmsApp->getScannerEvents().scanInProgress.emit(stats); - LmsApp->triggerUpdate(); - }); - }); + scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats) + { + postAll(server, [=] + { + LmsApp->getScannerEvents().scanInProgress.emit(stats); + LmsApp->triggerUpdate(); + }); + }); - scanner.getEvents().scanScheduled.connect([&] (const Wt::WDateTime dateTime) - { - postAll(server, [=] - { - LmsApp->getScannerEvents().scanScheduled.emit(dateTime); - LmsApp->triggerUpdate(); - }); - }); + scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime) + { + postAll(server, [=] + { + LmsApp->getScannerEvents().scanScheduled.emit(dateTime); + LmsApp->triggerUpdate(); + }); + }); } int main(int argc, char* argv[]) { - std::filesystem::path configFilePath {"/etc/lms.conf"}; - int res {EXIT_FAILURE}; + std::filesystem::path configFilePath{ "/etc/lms.conf" }; + int res{ EXIT_FAILURE }; - assert(argc > 0); - assert(argv[0] != NULL); + assert(argc > 0); + assert(argv[0] != NULL); - if (argc == 2) - configFilePath = std::string(argv[1], 0, 256); - else if (argc > 2) - { - std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n" - << "Options:\n" - << "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n"; - return EXIT_FAILURE; - } + if (argc == 2) + configFilePath = std::string(argv[1], 0, 256); + else if (argc > 2) + { + std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n" + << "Options:\n" + << "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n"; + return EXIT_FAILURE; + } - try - { - close(STDIN_FILENO); + try + { + close(STDIN_FILENO); - Service config {createConfig(configFilePath)}; - Service logger {std::make_unique()}; + Service config{ createConfig(configFilePath) }; + Service logger{ std::make_unique() }; - // use system locale. libarchive relies on this to write filenames - if (char *locale {::setlocale(LC_ALL, "")}) - LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'"; - else - LMS_LOG(MAIN, WARNING) << "Cannot set locale from system"; + // use system locale. libarchive relies on this to write filenames + if (char* locale{ ::setlocale(LC_ALL, "") }) + LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'"; + else + LMS_LOG(MAIN, WARNING) << "Cannot set locale from system"; - // Make sure the working directory exists - std::filesystem::create_directories(config->getPath("working-dir")); - std::filesystem::create_directories(config->getPath("working-dir") / "cache"); + // Make sure the working directory exists + std::filesystem::create_directories(config->getPath("working-dir")); + std::filesystem::create_directories(config->getPath("working-dir") / "cache"); - // Construct WT configuration and get the argc/argv back - const std::vector wtServerArgs {generateWtConfig(argv[0])}; + // Construct WT configuration and get the argc/argv back + const std::vector wtServerArgs{ generateWtConfig(argv[0]) }; - std::vector wtArgv(wtServerArgs.size()); - for (std::size_t i = 0; i < wtServerArgs.size(); ++i) - { - std::cout << "ARG = " << wtServerArgs[i] << std::endl; - wtArgv[i] = wtServerArgs[i].c_str(); - } + std::vector wtArgv(wtServerArgs.size()); + for (std::size_t i = 0; i < wtServerArgs.size(); ++i) + { + std::cout << "ARG = " << wtServerArgs[i] << std::endl; + wtArgv[i] = wtServerArgs[i].c_str(); + } - boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop - Wt::WServer server {argv[0]}; - server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); + boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop + Wt::WServer server{ argv[0] }; + server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); - IOContextRunner ioContextRunner {ioContext, getThreadCount()}; + IOContextRunner ioContextRunner{ ioContext, getThreadCount() }; - // Initializing a connection pool to the database that will be shared along services - Database::Db database {config->getPath("working-dir") / "lms.db", getThreadCount()}; - { - Database::Session session {database}; - session.prepareTables(); + // Initializing a connection pool to the database that will be shared along services + Database::Db database{ config->getPath("working-dir") / "lms.db", getThreadCount() }; + { + Database::Session session{ database }; + session.prepareTables(); - // force optimize in case scanner aborted during a large import: - // queries may be too slow to even be able to relaunch a scan sing the web interface - session.analyze(); - } + // force optimize in case scanner aborted during a large import: + // queries may be too slow to even be able to relaunch a scan sing the web interface + session.analyze(); + } - UserInterface::LmsApplicationManager appManager; + UserInterface::LmsApplicationManager appManager; - // Service initialization order is important (reverse-order for deinit) - Service childProcessManagerService {createChildProcessManager(ioContext)}; - Service authTokenService; - Service authPasswordService; - Service authEnvService; + // Service initialization order is important (reverse-order for deinit) + Service childProcessManagerService{ createChildProcessManager(ioContext) }; + Service authTokenService; + Service authPasswordService; + Service authEnvService; - const std::string authenticationBackend {StringUtils::stringToLower(config->getString("authentication-backend", "internal"))}; - if (authenticationBackend == "internal" || authenticationBackend == "pam") - { - authTokenService.assign(Auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000))); - authPasswordService.assign(Auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); - } - else if (authenticationBackend == "http-headers") - { - authEnvService.assign(Auth::createEnvService(authenticationBackend, database)); - } - else - throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"}; + const std::string authenticationBackend{ StringUtils::stringToLower(config->getString("authentication-backend", "internal")) }; + if (authenticationBackend == "internal" || authenticationBackend == "pam") + { + authTokenService.assign(Auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000))); + authPasswordService.assign(Auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); + } + else if (authenticationBackend == "http-headers") + { + authEnvService.assign(Auth::createEnvService(authenticationBackend, database)); + } + else + throw LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" }; - Image::init(argv[0]); - Service coverService {Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")}; - Service recommendationService {Recommendation::createRecommendationService(database)}; - Service playlistGeneratorService {Recommendation::createPlaylistGeneratorService(database, *recommendationService.get())}; - Service scannerService {Scanner::createScannerService(database, *recommendationService)}; + Image::init(argv[0]); + Service coverService{ Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") }; + Service recommendationService{ Recommendation::createRecommendationService(database) }; + Service playlistGeneratorService{ Recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; + Service scannerService{ Scanner::createScannerService(database, *recommendationService) }; - scannerService->getEvents().scanComplete.connect([&] - { - // Flush cover cache even if no changes: - // covers may be external files that changed and we don't keep track of them - coverService->flushCache(); - }); + scannerService->getEvents().scanComplete.connect([&] + { + // Flush cover cache even if no changes: + // covers may be external files that changed and we don't keep track of them + coverService->flushCache(); + }); - Service scrobblingService {Scrobbling::createScrobblingService(ioContext, database)}; + Service feedbackService{ Feedback::createFeedbackService(ioContext, database) }; + Service scrobblingService{ Scrobbling::createScrobblingService(ioContext, database) }; - std::unique_ptr subsonicResource; + std::unique_ptr subsonicResource; - // bind API resources - if (config->getBool("api-subsonic", true)) - { - subsonicResource = API::Subsonic::createSubsonicResource(database); - server.addResource(subsonicResource.get(), "/rest"); - } + // bind API resources + if (config->getBool("api-subsonic", true)) + { + subsonicResource = API::Subsonic::createSubsonicResource(database); + server.addResource(subsonicResource.get(), "/rest"); + } - // bind UI entry point - server.addEntryPoint(Wt::EntryPointType::Application, - [&](const Wt::WEnvironment &env) - { - return UserInterface::LmsApplication::create(env, database, appManager); - }); + // bind UI entry point + server.addEntryPoint(Wt::EntryPointType::Application, + [&](const Wt::WEnvironment& env) + { + return UserInterface::LmsApplication::create(env, database, appManager); + }); - proxyScannerEventsToApplication(*scannerService, server); + proxyScannerEventsToApplication(*scannerService, server); - LMS_LOG(MAIN, INFO) << "Starting server..."; - server.start(); + LMS_LOG(MAIN, INFO) << "Starting server..."; + server.start(); - LMS_LOG(MAIN, INFO) << "Now running..."; - Wt::WServer::waitForShutdown(); + LMS_LOG(MAIN, INFO) << "Now running..."; + Wt::WServer::waitForShutdown(); - LMS_LOG(MAIN, INFO) << "Stopping server..."; - server.stop(); + LMS_LOG(MAIN, INFO) << "Stopping server..."; + server.stop(); - LMS_LOG(MAIN, INFO) << "Quitting..."; - res = EXIT_SUCCESS; - } - catch (const Wt::WServer::Exception& e) - { - LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what(); - std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl; - res = EXIT_FAILURE; - } - catch (const std::exception& e) - { - LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what(); - std::cerr << "Caught std::exception: " << e.what() << std::endl; - res = EXIT_FAILURE; - } + LMS_LOG(MAIN, INFO) << "Quitting..."; + res = EXIT_SUCCESS; + } + catch (const Wt::WServer::Exception& e) + { + LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what(); + std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl; + res = EXIT_FAILURE; + } + catch (const std::exception& e) + { + LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what(); + std::cerr << "Caught std::exception: " << e.what() << std::endl; + res = EXIT_FAILURE; + } - return res; + return res; } diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index 1df4bc79..94d5cf34 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -34,7 +34,7 @@ #include "services/database/Track.hpp" #include "services/database/TrackList.hpp" #include "services/database/User.hpp" -#include "services/scrobbling/IScrobblingService.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IPlaylistGeneratorService.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" @@ -602,7 +602,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) loadTrack(*pos, true); }); - auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; + auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; Wt::WPushButton* starBtn {entry->bindNew("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; starBtn->clicked().connect([=] @@ -611,12 +611,12 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry) if (isStarred()) { - Service::get()->unstar(LmsApp->getUserId(), trackId); + Service::get()->unstar(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.star")); } else { - Service::get()->star(LmsApp->getUserId(), trackId); + Service::get()->star(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); } }); diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index cda71da2..03224b66 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -46,506 +46,540 @@ namespace UserInterface { -using namespace Database; - -class SettingsModel : public Wt::WFormModel -{ - public: - // Associate each field with a unique string literal. - static inline const Field TranscodeModeField {"transcode-mode"}; - static inline const Field TranscodeFormatField {"transcode-format"}; - static inline const Field TranscodeBitrateField {"transcode-bitrate"}; - static inline const Field ReplayGainModeField {"replaygain-mode"}; - static inline const Field ReplayGainPreAmpGainField {"replaygain-preamp"}; - static inline const Field ReplayGainPreAmpGainIfNoInfoField {"replaygain-preamp-no-rg-info"}; - static inline const Field SubsonicArtistListModeField {"subsonic-artist-list-mode"}; - static inline const Field SubsonicTranscodeEnableField {"subsonic-transcode-enable"}; - static inline const Field SubsonicTranscodeFormatField {"subsonic-transcode-format"}; - static inline const Field SubsonicTranscodeBitrateField {"subsonic-transcode-bitrate"}; - static inline const Field ScrobblerField {"scrobbler"}; - static inline const Field ListenBrainzTokenField {"listenbrainz-token"}; - static inline const Field PasswordOldField {"password-old"}; - static inline const Field PasswordField {"password"}; - static inline const Field PasswordConfirmField {"password-confirm"}; - - using TranscodeModeModel = ValueStringModel; - using ReplayGainModeModel = ValueStringModel; - using ScrobblerModel = ValueStringModel; - - SettingsModel(::Auth::IPasswordService* authPasswordService, bool withOldPassword) - : _authPasswordService {authPasswordService} - , _withOldPassword {withOldPassword} - { - initializeModels(); - - addField(TranscodeModeField); - addField(TranscodeBitrateField); - addField(TranscodeFormatField); - addField(ReplayGainModeField); - addField(ReplayGainPreAmpGainField); - addField(ReplayGainPreAmpGainIfNoInfoField); - addField(SubsonicTranscodeEnableField); - addField(SubsonicTranscodeBitrateField); - addField(SubsonicTranscodeFormatField); - addField(ScrobblerField); - addField(ListenBrainzTokenField); - setValidator(ListenBrainzTokenField, createUUIDValidator()); - - if (_authPasswordService) - { - if (_withOldPassword) - { - addField(PasswordOldField); - setValidator(PasswordOldField, createPasswordCheckValidator()); - } - - addField(PasswordField); - setValidator(PasswordField, createPasswordStrengthValidator([] { return ::Auth::PasswordValidationContext {LmsApp->getUserLoginName(), LmsApp->getUserType()}; })); - addField(PasswordConfirmField); - } - - setValidator(TranscodeModeField, createMandatoryValidator()); - setValidator(TranscodeBitrateField, createMandatoryValidator()); - setValidator(TranscodeFormatField, createMandatoryValidator()); - setValidator(ReplayGainModeField, createMandatoryValidator()); - auto createPreAmpValidator {[] - { - return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - }}; - - setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); - setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); - setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); - setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); - - loadData(); - } - - std::shared_ptr getTranscodeModeModel() { return _transcodeModeModel; } - std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } - std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } - std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } - std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } - std::shared_ptr getScrobblerModel() { return _scrobblerModel; } - - void saveData() - { - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - - User::pointer user {LmsApp->getUser()}; - - { - MediaPlayer::Settings settings; - - auto transcodeModeRow {_transcodeModeModel->getRowFromString(valueText(TranscodeModeField))}; - if (transcodeModeRow) - settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow); - - auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; - if (transcodeFormatRow) - settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow); - - auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; - if (transcodeBitrateRow) - settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); - - auto replayGainModeRow {_replayGainModeModel->getRowFromString(valueText(ReplayGainModeField))}; - if (replayGainModeRow) - settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); - - settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); - settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField)); - - LmsApp->getMediaPlayer().setSettings(settings); - } - - { - auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField))}; - if (subsonicTranscodeBitrateRow) - user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); - - auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField))}; - if (subsonicTranscodeFormatRow) - user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); - - auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))}; - if (subsonicArtistListModeRow) - user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); - - } - - { - if (auto scrobblerRow {_scrobblerModel->getRowFromString(valueText(ScrobblerField))}) - user.modify()->setScrobbler(_scrobblerModel->getValue(*scrobblerRow)); - - user.modify()->setListenBrainzToken(UUID::fromString(Wt::asString(value(ListenBrainzTokenField)).toUTF8())); - } - - if (_authPasswordService && !valueText(PasswordField).empty()) - { - _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); - } - } - - void loadData() - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - User::pointer user {LmsApp->getUser()}; - - { - const auto settings {*LmsApp->getMediaPlayer().getSettings()}; - - auto transcodeModeRow {_transcodeModeModel->getRowFromValue(settings.transcode.mode)}; - if (transcodeModeRow) - setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); - - auto transcodeFormatRow {_transcodeFormatModel->getRowFromValue(settings.transcode.format)}; - if (transcodeFormatRow) - setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); - - auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate)}; - if (transcodeBitrateRow) - setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); - - { - const bool usesTranscode {settings.transcode.mode != MediaPlayer::Settings::Transcode::Mode::Never}; - setReadOnly(SettingsModel::TranscodeFormatField, !usesTranscode); - setReadOnly(SettingsModel::TranscodeBitrateField, !usesTranscode); - } - - auto replayGainModeRow {_replayGainModeModel->getRowFromValue(settings.replayGain.mode)}; - if (replayGainModeRow) - setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); - - setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); - setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); - } - - { - auto subsonicTranscodeBitrateRow {_transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate())}; - if (subsonicTranscodeBitrateRow) - setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); - - auto subsonicTranscodeFormatRow {_transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat())}; - if (subsonicTranscodeFormatRow) - setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); - - auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode())}; - if (subsonicArtistListModeRow) - setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow)); - } - - { - if (auto scrobblerRow {_scrobblerModel->getRowFromValue(user->getScrobbler())}) - setValue(ScrobblerField, _scrobblerModel->getString(*scrobblerRow)); - - if (auto listenBrainzToken {user->getListenBrainzToken()}) - { - LMS_LOG(UI, DEBUG) << "Read listenBrainzToken! value = " << listenBrainzToken->getAsString(); - setValue(ListenBrainzTokenField, Wt::WString::fromUTF8( std::string {listenBrainzToken->getAsString()})); - } - - { - const bool usesListenBrainz {user->getScrobbler() == Scrobbler::ListenBrainz}; - setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); - validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); - } - } - if (_authPasswordService) - { - if (_withOldPassword) - setValue(PasswordOldField, ""); - - setValue(PasswordField, ""); - setValue(PasswordConfirmField, ""); - } - } - - private: - bool validateField(Field field) - { - Wt::WString error; - - if (field == PasswordOldField) - { - if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty()) - error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); - else - return Wt::WFormModel::validateField(field); - } - else if (field == PasswordField) - { - if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty()) - error = Wt::WString::tr("Wt.WValidator.Invalid"); - else - return Wt::WFormModel::validateField(field); - } - else if (field == PasswordConfirmField) - { - if (validation(PasswordField).state() == Wt::ValidationState::Valid) - { - if (valueText(PasswordField) != valueText(PasswordConfirmField)) - error = Wt::WString::tr("Lms.passwords-dont-match"); - } - } - else - { - return Wt::WFormModel::validateField(field); - } - - setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error)); - - return (validation(field).state() == Wt::ValidationState::Valid); - } - - private: - - void initializeModels() - { - _transcodeModeModel = std::make_shared(); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never); - _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); - - _transcodeBitrateModel = std::make_shared>(); - 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); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS); - _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS); - - _replayGainModeModel = std::make_shared(); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); - _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto); - _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"), 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); - _scrobblerModel->add(Wt::WString::tr("Lms.Settings.scrobbling.scrobbler.listenbrainz"), Scrobbler::ListenBrainz); - } - - ::Auth::IPasswordService* _authPasswordService {}; - bool _withOldPassword {}; - - std::shared_ptr _transcodeModeModel; - std::shared_ptr> _transcodeBitrateModel; - std::shared_ptr> _transcodeFormatModel; - std::shared_ptr _replayGainModeModel; - std::shared_ptr> _subsonicArtistListModeModel; - std::shared_ptr _scrobblerModel; -}; - -SettingsView::SettingsView() -{ - wApp->internalPathChanged().connect(this, [this] - { - refreshView(); - }); - - LmsApp->getMediaPlayer().settingsLoaded.connect([this] - { - refreshView(); - }); - - refreshView(); -} - -void -SettingsView::refreshView() -{ - if (!wApp->internalPathMatches("/settings")) - return; - - clear(); - - // Hack to wait for the audio player know the settings applied - if (!LmsApp->getMediaPlayer().getSettings()) - return; - - auto t {addNew(Wt::WString::tr("Lms.Settings.template"))}; - - auto* authPasswordService {Service<::Auth::IPasswordService>::get()}; - if (authPasswordService && !authPasswordService->canSetPasswords()) - authPasswordService = nullptr; - - auto model {std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong())}; - - if (authPasswordService) - { - t->setCondition("if-has-change-password", true); - - // Old password - if (!LmsApp->isUserAuthStrong()) - { - t->setCondition("if-has-old-password", true); - - auto oldPassword {std::make_unique()}; - oldPassword->setEchoMode(Wt::EchoMode::Password); - oldPassword->setAttributeValue("autocomplete", "current-password"); - t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); - } - - // Password - auto password {std::make_unique()}; - password->setEchoMode(Wt::EchoMode::Password); - password->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordField, std::move(password)); - - // Password confirm - auto passwordConfirm {std::make_unique()}; - passwordConfirm->setEchoMode(Wt::EchoMode::Password); - passwordConfirm->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); - } - - // Audio - { - // Transcode - auto transcodeMode {std::make_unique()}; - auto* transcodeModeRaw {transcodeMode.get()}; - transcodeMode->setModel(model->getTranscodeModeModel()); - t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode)); - - // Format - auto transcodeFormat {std::make_unique()}; - transcodeFormat->setModel(model->getTranscodeFormatModel()); - t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat)); - - // Bitrate - auto transcodeBitrate {std::make_unique()}; - transcodeBitrate->setModel(model->getTranscodeBitrateModel()); - t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); - - transcodeModeRaw->activated().connect([=](int row) - { - const bool enable {model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never}; - model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); - model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - - // Replay gain mode - auto replayGainMode {std::make_unique()}; - auto* replayGainModeRaw {replayGainMode.get()}; - replayGainMode->setModel(model->getReplayGainModeModel()); - t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode)); - - // Replay gain preampGain - auto replayGainPreampGain {std::make_unique()}; - replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); - - // Replay gain preampGain if no info - auto replayGainPreampGainIfNoInfo {std::make_unique()}; - replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); - t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo)); - - replayGainModeRaw->activated().connect([=](int row) - { - const bool enable {model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None}; - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable); - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None) - { - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true); - model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, true); - } - } - - // Subsonic - { - t->setCondition("if-has-subsonic-api", Service::get()->getBool("api-subsonic", true)); - - // Format - auto transcodeFormat {std::make_unique()}; - transcodeFormat->setModel(model->getTranscodeFormatModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat)); - - // Bitrate - auto transcodeBitrate {std::make_unique()}; - transcodeBitrate->setModel(model->getTranscodeBitrateModel()); - t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate)); - - // Artist list mode - auto artistListMode {std::make_unique()}; - artistListMode->setModel(model->getSubsonicArtistListModeModel()); - t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); - } - - // Scrobbling - { - auto scrobbler {std::make_unique()}; - scrobbler->setModel(model->getScrobblerModel()); - auto* scrobblerRaw {scrobbler.get()}; - t->setFormWidget(SettingsModel::ScrobblerField, std::move(scrobbler)); - - auto listenbrainzToken {std::make_unique()}; - t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); - scrobblerRaw->activated().connect([=](int row) - { - const bool enable {model->getScrobblerModel()->getValue(row) == Scrobbler::ListenBrainz}; - model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable); - model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - } - - // Buttons - Wt::WPushButton *saveBtn {t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply")))}; - Wt::WPushButton *discardBtn {t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard")))}; - - saveBtn->clicked().connect([=] - { - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - if (LmsApp->getUser()->isDemo()) - { - LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.demo-cannot-save")); - return; - } - } - - t->updateModel(model.get()); - - if (model->validate()) - { - model->saveData(); - LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.settings-saved")); - } - - // Udate the view: Delete any validation message in the view, etc. - t->updateView(model.get()); - }); - - discardBtn->clicked().connect([=] - { - model->loadData(); - model->validate(); - t->updateView(model.get()); - }); - - t->updateView(model.get()); -} - -} // namespace UserInterface - - + using namespace Database; + + class SettingsModel : public Wt::WFormModel + { + public: + // Associate each field with a unique string literal. + static inline const Field TranscodeModeField{ "transcode-mode" }; + static inline const Field TranscodeFormatField{ "transcode-format" }; + static inline const Field TranscodeBitrateField{ "transcode-bitrate" }; + static inline const Field ReplayGainModeField{ "replaygain-mode" }; + static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" }; + static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" }; + static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; + static inline const Field SubsonicTranscodeFormatField{ "subsonic-transcode-format" }; + static inline const Field SubsonicTranscodeBitrateField{ "subsonic-transcode-bitrate" }; + static inline const Field FeedbackBackendField{ "feedback-backend" }; + static inline const Field ScrobblingBackendField{ "scrobbling-backend" }; + static inline const Field ListenBrainzTokenField{ "listenbrainz-token" }; + static inline const Field PasswordOldField{ "password-old" }; + static inline const Field PasswordField{ "password" }; + static inline const Field PasswordConfirmField{ "password-confirm" }; + + using TranscodeModeModel = ValueStringModel; + using ReplayGainModeModel = ValueStringModel; + using FeedbackBackendModel = ValueStringModel; + using ScrobblingBackendModel = ValueStringModel; + + SettingsModel(::Auth::IPasswordService* authPasswordService, bool withOldPassword) + : _authPasswordService{ authPasswordService } + , _withOldPassword{ withOldPassword } + { + initializeModels(); + + addField(TranscodeModeField); + addField(TranscodeBitrateField); + addField(TranscodeFormatField); + addField(ReplayGainModeField); + addField(ReplayGainPreAmpGainField); + addField(ReplayGainPreAmpGainIfNoInfoField); + addField(SubsonicTranscodeBitrateField); + addField(SubsonicTranscodeFormatField); + addField(FeedbackBackendField); + addField(ScrobblingBackendField); + addField(ListenBrainzTokenField); + setValidator(ListenBrainzTokenField, createUUIDValidator()); + + if (_authPasswordService) + { + if (_withOldPassword) + { + addField(PasswordOldField); + setValidator(PasswordOldField, createPasswordCheckValidator()); + } + + addField(PasswordField); + setValidator(PasswordField, createPasswordStrengthValidator([] { return ::Auth::PasswordValidationContext{ LmsApp->getUserLoginName(), LmsApp->getUserType() }; })); + addField(PasswordConfirmField); + } + + setValidator(TranscodeModeField, createMandatoryValidator()); + setValidator(TranscodeBitrateField, createMandatoryValidator()); + setValidator(TranscodeFormatField, createMandatoryValidator()); + setValidator(ReplayGainModeField, createMandatoryValidator()); + auto createPreAmpValidator{ [] + { + return createDoubleValidator(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + } }; + + setValidator(ReplayGainPreAmpGainField, createPreAmpValidator()); + setValidator(ReplayGainPreAmpGainIfNoInfoField, createPreAmpValidator()); + setValidator(SubsonicTranscodeBitrateField, createMandatoryValidator()); + setValidator(SubsonicTranscodeFormatField, createMandatoryValidator()); + + loadData(); + } + + std::shared_ptr getTranscodeModeModel() { return _transcodeModeModel; } + std::shared_ptr getTranscodeBitrateModel() { return _transcodeBitrateModel; } + std::shared_ptr getTranscodeFormatModel() { return _transcodeFormatModel; } + std::shared_ptr getReplayGainModeModel() { return _replayGainModeModel; } + std::shared_ptr getSubsonicArtistListModeModel() { return _subsonicArtistListModeModel; } + std::shared_ptr getFeedbackBackendModel() { return _feedbackBackendModel; } + std::shared_ptr getScrobblingBackendModel() { return _scrobblingBackendModel; } + + void saveData() + { + auto transaction{ LmsApp->getDbSession().createUniqueTransaction() }; + + User::pointer user{ LmsApp->getUser() }; + + { + MediaPlayer::Settings settings; + + auto transcodeModeRow{ _transcodeModeModel->getRowFromString(valueText(TranscodeModeField)) }; + if (transcodeModeRow) + settings.transcode.mode = _transcodeModeModel->getValue(*transcodeModeRow); + + auto transcodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField)) }; + if (transcodeFormatRow) + settings.transcode.format = _transcodeFormatModel->getValue(*transcodeFormatRow); + + auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField)) }; + if (transcodeBitrateRow) + settings.transcode.bitrate = _transcodeBitrateModel->getValue(*transcodeBitrateRow); + + auto replayGainModeRow{ _replayGainModeModel->getRowFromString(valueText(ReplayGainModeField)) }; + if (replayGainModeRow) + settings.replayGain.mode = _replayGainModeModel->getValue(*replayGainModeRow); + + settings.replayGain.preAmpGain = Wt::asNumber(value(ReplayGainPreAmpGainField)); + settings.replayGain.preAmpGainIfNoInfo = Wt::asNumber(value(ReplayGainPreAmpGainIfNoInfoField)); + + LmsApp->getMediaPlayer().setSettings(settings); + } + + { + auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromString(valueText(SubsonicTranscodeBitrateField)) }; + if (subsonicTranscodeBitrateRow) + user.modify()->setSubsonicDefaultTranscodeBitrate(_transcodeBitrateModel->getValue(*subsonicTranscodeBitrateRow)); + + auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromString(valueText(SubsonicTranscodeFormatField)) }; + if (subsonicTranscodeFormatRow) + user.modify()->setSubsonicDefaultTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); + + auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField)) }; + if (subsonicArtistListModeRow) + user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); + + } + + { + if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromString(valueText(FeedbackBackendField)) }) + user.modify()->setFeedbackBackend(_feedbackBackendModel->getValue(*feedbackBackendRow)); + + user.modify()->setListenBrainzToken(UUID::fromString(Wt::asString(value(ListenBrainzTokenField)).toUTF8())); + } + + { + if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromString(valueText(ScrobblingBackendField)) }) + user.modify()->setScrobblingBackend(_scrobblingBackendModel->getValue(*scrobblingBackendRow)); + + user.modify()->setListenBrainzToken(UUID::fromString(Wt::asString(value(ListenBrainzTokenField)).toUTF8())); + } + + if (_authPasswordService && !valueText(PasswordField).empty()) + { + _authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8()); + } + } + + void loadData() + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + + User::pointer user{ LmsApp->getUser() }; + + { + const auto settings{ *LmsApp->getMediaPlayer().getSettings() }; + + auto transcodeModeRow{ _transcodeModeModel->getRowFromValue(settings.transcode.mode) }; + if (transcodeModeRow) + setValue(TranscodeModeField, _transcodeModeModel->getString(*transcodeModeRow)); + + auto transcodeFormatRow{ _transcodeFormatModel->getRowFromValue(settings.transcode.format) }; + if (transcodeFormatRow) + setValue(TranscodeFormatField, _transcodeFormatModel->getString(*transcodeFormatRow)); + + auto transcodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(settings.transcode.bitrate) }; + if (transcodeBitrateRow) + setValue(TranscodeBitrateField, _transcodeBitrateModel->getString(*transcodeBitrateRow)); + + { + const bool usesTranscode{ settings.transcode.mode != MediaPlayer::Settings::Transcode::Mode::Never }; + setReadOnly(SettingsModel::TranscodeFormatField, !usesTranscode); + setReadOnly(SettingsModel::TranscodeBitrateField, !usesTranscode); + } + + auto replayGainModeRow{ _replayGainModeModel->getRowFromValue(settings.replayGain.mode) }; + if (replayGainModeRow) + setValue(ReplayGainModeField, _replayGainModeModel->getString(*replayGainModeRow)); + + setValue(ReplayGainPreAmpGainField, settings.replayGain.preAmpGain); + setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo); + } + + { + auto subsonicTranscodeBitrateRow{ _transcodeBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodeBitrate()) }; + if (subsonicTranscodeBitrateRow) + setValue(SubsonicTranscodeBitrateField, _transcodeBitrateModel->getString(*subsonicTranscodeBitrateRow)); + + auto subsonicTranscodeFormatRow{ _transcodeFormatModel->getRowFromValue(user->getSubsonicDefaultTranscodeFormat()) }; + if (subsonicTranscodeFormatRow) + setValue(SubsonicTranscodeFormatField, _transcodeFormatModel->getString(*subsonicTranscodeFormatRow)); + + auto subsonicArtistListModeRow{ _subsonicArtistListModeModel->getRowFromValue(user->getSubsonicArtistListMode()) }; + if (subsonicArtistListModeRow) + setValue(SubsonicArtistListModeField, _subsonicArtistListModeModel->getString(*subsonicArtistListModeRow)); + } + + { + if (auto feedbackBackendRow{ _feedbackBackendModel->getRowFromValue(user->getFeedbackBackend()) }) + setValue(FeedbackBackendField, _feedbackBackendModel->getString(*feedbackBackendRow)); + + if (auto scrobblingBackendRow{ _scrobblingBackendModel->getRowFromValue(user->getScrobblingBackend()) }) + setValue(ScrobblingBackendField, _scrobblingBackendModel->getString(*scrobblingBackendRow)); + + if (auto listenBrainzToken{ user->getListenBrainzToken() }) + { + LMS_LOG(UI, DEBUG) << "Read listenBrainzToken! value = " << listenBrainzToken->getAsString(); + setValue(ListenBrainzTokenField, Wt::WString::fromUTF8(std::string{ listenBrainzToken->getAsString() })); + } + + { + const bool usesListenBrainz{ user->getScrobblingBackend() == ScrobblingBackend::ListenBrainz || user->getFeedbackBackend() == FeedbackBackend::ListenBrainz}; + setReadOnly(SettingsModel::ListenBrainzTokenField, !usesListenBrainz); + validator(SettingsModel::ListenBrainzTokenField)->setMandatory(usesListenBrainz); + } + } + if (_authPasswordService) + { + if (_withOldPassword) + setValue(PasswordOldField, ""); + + setValue(PasswordField, ""); + setValue(PasswordConfirmField, ""); + } + } + + private: + bool validateField(Field field) + { + Wt::WString error; + + if (field == PasswordOldField) + { + if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty()) + error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); + else + return Wt::WFormModel::validateField(field); + } + else if (field == PasswordField) + { + if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty()) + error = Wt::WString::tr("Wt.WValidator.Invalid"); + else + return Wt::WFormModel::validateField(field); + } + else if (field == PasswordConfirmField) + { + if (validation(PasswordField).state() == Wt::ValidationState::Valid) + { + if (valueText(PasswordField) != valueText(PasswordConfirmField)) + error = Wt::WString::tr("Lms.passwords-dont-match"); + } + } + else + { + return Wt::WFormModel::validateField(field); + } + + setValidation(field, Wt::WValidator::Result(error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error)); + + return (validation(field).state() == Wt::ValidationState::Valid); + } + + private: + + void initializeModels() + { + _transcodeModeModel = std::make_shared(); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.always"), MediaPlayer::Settings::Transcode::Mode::Always); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.never"), MediaPlayer::Settings::Transcode::Mode::Never); + _transcodeModeModel->add(Wt::WString::tr("Lms.Settings.transcode-mode.if-format-not-supported"), MediaPlayer::Settings::Transcode::Mode::IfFormatNotSupported); + + _transcodeBitrateModel = std::make_shared>(); + 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); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_opus"), AudioFormat::OGG_OPUS); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.matroska_opus"), AudioFormat::MATROSKA_OPUS); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.ogg_vorbis"), AudioFormat::OGG_VORBIS); + _transcodeFormatModel->add(Wt::WString::tr("Lms.Settings.transcode-format.webm_vorbis"), AudioFormat::WEBM_VORBIS); + + _replayGainModeModel = std::make_shared(); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.none"), MediaPlayer::Settings::ReplayGain::Mode::None); + _replayGainModeModel->add(Wt::WString::tr("Lms.Settings.replaygain-mode.auto"), MediaPlayer::Settings::ReplayGain::Mode::Auto); + _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"), 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); + + _feedbackBackendModel = std::make_shared>(); + _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), FeedbackBackend::Internal); + _feedbackBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), FeedbackBackend::ListenBrainz); + + _scrobblingBackendModel = std::make_shared>(); + _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.internal"), ScrobblingBackend::Internal); + _scrobblingBackendModel->add(Wt::WString::tr("Lms.Settings.backend.listenbrainz"), ScrobblingBackend::ListenBrainz); + } + + ::Auth::IPasswordService* _authPasswordService{}; + bool _withOldPassword{}; + + std::shared_ptr _transcodeModeModel; + std::shared_ptr> _transcodeBitrateModel; + std::shared_ptr> _transcodeFormatModel; + std::shared_ptr _replayGainModeModel; + std::shared_ptr> _subsonicArtistListModeModel; + std::shared_ptr _feedbackBackendModel; + std::shared_ptr _scrobblingBackendModel; + }; + + SettingsView::SettingsView() + { + wApp->internalPathChanged().connect(this, [this] + { + refreshView(); + }); + + LmsApp->getMediaPlayer().settingsLoaded.connect([this] + { + refreshView(); + }); + + refreshView(); + } + + void + SettingsView::refreshView() + { + if (!wApp->internalPathMatches("/settings")) + return; + + clear(); + + // Hack to wait for the audio player know the settings applied + if (!LmsApp->getMediaPlayer().getSettings()) + return; + + auto t{ addNew(Wt::WString::tr("Lms.Settings.template")) }; + + auto* authPasswordService{ Service<::Auth::IPasswordService>::get() }; + if (authPasswordService && !authPasswordService->canSetPasswords()) + authPasswordService = nullptr; + + auto model{ std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong()) }; + + if (authPasswordService) + { + t->setCondition("if-has-change-password", true); + + // Old password + if (!LmsApp->isUserAuthStrong()) + { + t->setCondition("if-has-old-password", true); + + auto oldPassword{ std::make_unique() }; + oldPassword->setEchoMode(Wt::EchoMode::Password); + oldPassword->setAttributeValue("autocomplete", "current-password"); + t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); + } + + // Password + auto password{ std::make_unique() }; + password->setEchoMode(Wt::EchoMode::Password); + password->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(SettingsModel::PasswordField, std::move(password)); + + // Password confirm + auto passwordConfirm{ std::make_unique() }; + passwordConfirm->setEchoMode(Wt::EchoMode::Password); + passwordConfirm->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); + } + + // Audio + { + // Transcode + auto transcodeMode{ std::make_unique() }; + auto* transcodeModeRaw{ transcodeMode.get() }; + transcodeMode->setModel(model->getTranscodeModeModel()); + t->setFormWidget(SettingsModel::TranscodeModeField, std::move(transcodeMode)); + + // Format + auto transcodeFormat{ std::make_unique() }; + transcodeFormat->setModel(model->getTranscodeFormatModel()); + t->setFormWidget(SettingsModel::TranscodeFormatField, std::move(transcodeFormat)); + + // Bitrate + auto transcodeBitrate{ std::make_unique() }; + transcodeBitrate->setModel(model->getTranscodeBitrateModel()); + t->setFormWidget(SettingsModel::TranscodeBitrateField, std::move(transcodeBitrate)); + + transcodeModeRaw->activated().connect([=](int row) + { + const bool enable{ model->getTranscodeModeModel()->getValue(row) != MediaPlayer::Settings::Transcode::Mode::Never }; + model->setReadOnly(SettingsModel::TranscodeFormatField, !enable); + model->setReadOnly(SettingsModel::TranscodeBitrateField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + + // Replay gain mode + auto replayGainMode{ std::make_unique() }; + auto* replayGainModeRaw{ replayGainMode.get() }; + replayGainMode->setModel(model->getReplayGainModeModel()); + t->setFormWidget(SettingsModel::ReplayGainModeField, std::move(replayGainMode)); + + // Replay gain preampGain + auto replayGainPreampGain{ std::make_unique() }; + replayGainPreampGain->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(SettingsModel::ReplayGainPreAmpGainField, std::move(replayGainPreampGain)); + + // Replay gain preampGain if no info + auto replayGainPreampGainIfNoInfo{ std::make_unique() }; + replayGainPreampGainIfNoInfo->setRange(MediaPlayer::Settings::ReplayGain::minPreAmpGain, MediaPlayer::Settings::ReplayGain::maxPreAmpGain); + t->setFormWidget(SettingsModel::ReplayGainPreAmpGainIfNoInfoField, std::move(replayGainPreampGainIfNoInfo)); + + replayGainModeRaw->activated().connect([=](int row) + { + const bool enable{ model->getReplayGainModeModel()->getValue(row) != MediaPlayer::Settings::ReplayGain::Mode::None }; + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, !enable); + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, !enable); + t->updateModel(model.get()); + t->updateView(model.get()); + }); + if (LmsApp->getMediaPlayer().getSettings()->replayGain.mode == MediaPlayer::Settings::ReplayGain::Mode::None) + { + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainField, true); + model->setReadOnly(SettingsModel::SettingsModel::ReplayGainPreAmpGainIfNoInfoField, true); + } + } + + // Subsonic + { + t->setCondition("if-has-subsonic-api", Service::get()->getBool("api-subsonic", true)); + + // Format + auto transcodeFormat{ std::make_unique() }; + transcodeFormat->setModel(model->getTranscodeFormatModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodeFormatField, std::move(transcodeFormat)); + + // Bitrate + auto transcodeBitrate{ std::make_unique() }; + transcodeBitrate->setModel(model->getTranscodeBitrateModel()); + t->setFormWidget(SettingsModel::SubsonicTranscodeBitrateField, std::move(transcodeBitrate)); + + // Artist list mode + auto artistListMode{ std::make_unique() }; + artistListMode->setModel(model->getSubsonicArtistListModeModel()); + t->setFormWidget(SettingsModel::SubsonicArtistListModeField, std::move(artistListMode)); + } + + // Feedback + Wt::WComboBox* feedbackBackendRaw{}; + { + auto feedbackBackend{ std::make_unique() }; + feedbackBackend->setModel(model->getFeedbackBackendModel()); + feedbackBackendRaw = feedbackBackend.get(); + t->setFormWidget(SettingsModel::FeedbackBackendField, std::move(feedbackBackend)); + } + + // Scrobbling + Wt::WComboBox* scrobblingBackendRaw; + { + auto scrobblingBackend{ std::make_unique() }; + scrobblingBackend->setModel(model->getScrobblingBackendModel()); + scrobblingBackendRaw = scrobblingBackend.get(); + t->setFormWidget(SettingsModel::ScrobblingBackendField, std::move(scrobblingBackend)); + } + + // Backend settings + { + auto listenbrainzToken{ std::make_unique() }; + t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); + } + + auto updateListenBrainzTokenField{ [=] + { + const bool enable{ model->getFeedbackBackendModel()->getValue(feedbackBackendRaw->currentIndex()) == FeedbackBackend::ListenBrainz + || model->getScrobblingBackendModel()->getValue(scrobblingBackendRaw->currentIndex()) == ScrobblingBackend::ListenBrainz }; + + model->setReadOnly(SettingsModel::ListenBrainzTokenField, !enable); + model->validator(SettingsModel::ListenBrainzTokenField)->setMandatory(enable); + t->updateModel(model.get()); + t->updateView(model.get()); + } }; + + feedbackBackendRaw->activated().connect([=] { updateListenBrainzTokenField(); }); + scrobblingBackendRaw->activated().connect([=] { updateListenBrainzTokenField();}); + + // Buttons + Wt::WPushButton* saveBtn{ t->bindWidget("apply-btn", std::make_unique(Wt::WString::tr("Lms.apply"))) }; + Wt::WPushButton* discardBtn{ t->bindWidget("discard-btn", std::make_unique(Wt::WString::tr("Lms.discard"))) }; + + saveBtn->clicked().connect([=] + { + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + + if (LmsApp->getUser()->isDemo()) + { + LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.demo-cannot-save")); + return; + } + } + + t->updateModel(model.get()); + + if (model->validate()) + { + model->saveData(); + LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.settings"), Wt::WString::tr("Lms.Settings.settings-saved")); + } + + // Udate the view: Delete any validation message in the view, etc. + t->updateView(model.get()); + }); + + discardBtn->clicked().connect([=] + { + model->loadData(); + model->validate(); + t->updateView(model.get()); + }); + + t->updateView(model.get()); + } + +} // namespace UserInterface \ No newline at end of file diff --git a/src/lms/ui/explore/ArtistCollector.cpp b/src/lms/ui/explore/ArtistCollector.cpp index 16bfd9c7..e05c5eba 100644 --- a/src/lms/ui/explore/ArtistCollector.cpp +++ b/src/lms/ui/explore/ArtistCollector.cpp @@ -23,6 +23,7 @@ #include "services/database/Session.hpp" #include "services/database/TrackList.hpp" #include "services/database/User.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "Filters.hpp" @@ -30,107 +31,105 @@ namespace UserInterface { - using namespace Database; + using namespace Database; - RangeResults - ArtistCollector::get(Database::Range range) - { - Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + RangeResults ArtistCollector::get(Database::Range range) + { + Feedback::IFeedbackService& feedbackService{ *Service::get() }; + Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; - range = getActualRange(range); + range = getActualRange(range); - RangeResults artists; - switch (getMode()) - { - case Mode::Random: - artists = getRandomArtists(range); - break; + RangeResults artists; + switch (getMode()) + { + case Mode::Random: + artists = getRandomArtists(range); + break; - case Mode::Starred: - artists = scrobbling.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); - break; + case Mode::Starred: + artists = feedbackService.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range); + break; - case Mode::RecentlyPlayed: - artists = scrobbling.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); - break; + case Mode::RecentlyPlayed: + artists = scrobblingService.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); + break; - case Mode::MostPlayed: - artists = scrobbling.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); - break; + case Mode::MostPlayed: + artists = scrobblingService.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range); + break; - case Mode::RecentlyAdded: - { - Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); - params.setLinkType(_linkType); - params.setSortMethod(ArtistSortMethod::LastWritten); - params.setRange(range); + case Mode::RecentlyAdded: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::LastWritten); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - artists = Artist::find(LmsApp->getDbSession(), params); - } - break; - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + artists = Artist::find(LmsApp->getDbSession(), params); + } + break; + } - case Mode::Search: - { - Artist::FindParameters params; - params.setClusters(getFilters().getClusterIds()); - params.setKeywords(getSearchKeywords()); - params.setLinkType(_linkType); - params.setSortMethod(ArtistSortMethod::BySortName); - params.setRange(range); + case Mode::Search: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - artists = Artist::find(LmsApp->getDbSession(), params); - } - break; - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + 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); + case Mode::All: + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::BySortName); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - artists = Artist::find(LmsApp->getDbSession(), params); - } - break; - } - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + artists = Artist::find(LmsApp->getDbSession(), params); + } + break; + } + } - if (range.offset + range.size == getMaxCount()) - artists.moreResults = false; + if (range.offset + range.size == getMaxCount()) + artists.moreResults = false; - return artists; - } + return artists; + } - RangeResults - ArtistCollector::getRandomArtists(Range range) - { - assert(getMode() == Mode::Random); + 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()}); + if (!_randomArtists) + { + Artist::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setLinkType(_linkType); + params.setSortMethod(ArtistSortMethod::Random); + params.setRange(Range{ 0, getMaxCount() }); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - _randomArtists = Artist::find(LmsApp->getDbSession(), params); - } - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + _randomArtists = Artist::find(LmsApp->getDbSession(), params); + } + } - return _randomArtists->getSubRange(range); - } + return _randomArtists->getSubRange(range); + } } // ns UserInterface - diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index d90dbc5e..9d9f7626 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -28,7 +28,7 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/User.hpp" -#include "services/scrobbling/IScrobblingService.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IRecommendationService.hpp" #include "utils/Logger.hpp" #include "utils/String.hpp" @@ -187,19 +187,19 @@ Artist::refreshView() ->setLink(Wt::WLink {std::make_unique(_artistId)}); { - auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), _artistId); }}; + auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), _artistId); }}; Wt::WPushButton* starBtn {bindNew("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; starBtn->clicked().connect([=] { if (isStarred()) { - Service::get()->unstar(LmsApp->getUserId(), _artistId); + Service::get()->unstar(LmsApp->getUserId(), _artistId); starBtn->setText(Wt::WString::tr("Lms.Explore.star")); } else { - Service::get()->star(LmsApp->getUserId(), _artistId); + Service::get()->star(LmsApp->getUserId(), _artistId); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); } }); diff --git a/src/lms/ui/explore/ReleaseCollector.cpp b/src/lms/ui/explore/ReleaseCollector.cpp index cb91cac9..9caea832 100644 --- a/src/lms/ui/explore/ReleaseCollector.cpp +++ b/src/lms/ui/explore/ReleaseCollector.cpp @@ -25,6 +25,7 @@ #include "services/database/Session.hpp" #include "services/database/User.hpp" #include "services/database/TrackList.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" #include "utils/Service.hpp" #include "Filters.hpp" @@ -32,104 +33,105 @@ namespace UserInterface { - using namespace Database; + using namespace Database; - RangeResults - ReleaseCollector::get(Database::Range range) - { - Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + RangeResults + ReleaseCollector::get(Database::Range range) + { + Feedback::IFeedbackService& feedbackService{ *Service::get() }; + Scrobbling::IScrobblingService& scrobblingService{ *Service::get() }; - range = getActualRange(range); + range = getActualRange(range); - RangeResults releases; - switch (getMode()) - { - case Mode::Random: - releases = getRandomReleases(range); - break; + RangeResults releases; + switch (getMode()) + { + case Mode::Random: + releases = getRandomReleases(range); + break; - case Mode::Starred: - releases = scrobbling.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); - break; + case Mode::Starred: + releases = feedbackService.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); + break; - case ReleaseCollector::Mode::RecentlyPlayed: - releases = scrobbling.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); - break; + case ReleaseCollector::Mode::RecentlyPlayed: + releases = scrobblingService.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); + break; - case Mode::MostPlayed: - releases = scrobbling.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); - break; + case Mode::MostPlayed: + releases = scrobblingService.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range); + break; - case Mode::RecentlyAdded: - { - Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); - params.setSortMethod(ReleaseSortMethod::LastWritten); - params.setRange(range); + case Mode::RecentlyAdded: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::LastWritten); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - releases = Release::find(LmsApp->getDbSession(), params); - } - break; - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + releases = Release::find(LmsApp->getDbSession(), params); + } + break; + } - case Mode::Search: - { - Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); - params.setKeywords(getSearchKeywords()); - params.setSortMethod(ReleaseSortMethod::Name); - params.setRange(range); + case Mode::Search: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setKeywords(getSearchKeywords()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - releases = Release::find(LmsApp->getDbSession(), params); - } - break; - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + releases = Release::find(LmsApp->getDbSession(), params); + } + break; + } - case Mode::All: - { - Release::FindParameters params; - params.setClusters(getFilters().getClusterIds()); - params.setSortMethod(ReleaseSortMethod::Name); - params.setRange(range); + case Mode::All: + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Name); + params.setRange(range); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - releases = Release::find(LmsApp->getDbSession(), params); - } - break; - } - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + releases = Release::find(LmsApp->getDbSession(), params); + } + break; + } + } - if (range.offset + range.size == getMaxCount()) - releases.moreResults = false; + if (range.offset + range.size == getMaxCount()) + releases.moreResults = false; - return releases; - } + return releases; + } - RangeResults - ReleaseCollector::getRandomReleases(Range range) - { - assert(getMode() == Mode::Random); + 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()}); + if (!_randomReleases) + { + Release::FindParameters params; + params.setClusters(getFilters().getClusterIds()); + params.setSortMethod(ReleaseSortMethod::Random); + params.setRange({ 0, getMaxCount() }); - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - _randomReleases = Release::find(LmsApp->getDbSession(), params); - } - } + { + auto transaction{ LmsApp->getDbSession().createSharedTransaction() }; + _randomReleases = Release::find(LmsApp->getDbSession(), params); + } + } - return _randomReleases->getSubRange(range); - } + return _randomReleases->getSubRange(range); + } } // ns UserInterface diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 4b5770ee..69710067 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -32,8 +32,8 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "services/recommendation/IRecommendationService.hpp" -#include "services/scrobbling/IScrobblingService.hpp" #include "utils/Logger.hpp" #include "common/Template.hpp" @@ -314,19 +314,19 @@ Release::refreshView() }); { - auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), _releaseId); }}; + auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), _releaseId); }}; Wt::WPushButton* starBtn {bindNew("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; starBtn->clicked().connect([=] { if (isStarred()) { - Service::get()->unstar(LmsApp->getUserId(), _releaseId); + Service::get()->unstar(LmsApp->getUserId(), _releaseId); starBtn->setText(Wt::WString::tr("Lms.Explore.star")); } else { - Service::get()->star(LmsApp->getUserId(), _releaseId); + Service::get()->star(LmsApp->getUserId(), _releaseId); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); } }); @@ -435,7 +435,7 @@ Release::refreshView() _playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId}); }); - auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; + auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; Wt::WPushButton* starBtn {entry->bindNew("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; starBtn->clicked().connect([=] @@ -444,12 +444,12 @@ Release::refreshView() if (isStarred()) { - Service::get()->unstar(LmsApp->getUserId(), trackId); + Service::get()->unstar(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.star")); } else { - Service::get()->star(LmsApp->getUserId(), trackId); + Service::get()->star(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); } }); diff --git a/src/lms/ui/explore/TrackCollector.cpp b/src/lms/ui/explore/TrackCollector.cpp index ed6d2bd4..596edcd7 100644 --- a/src/lms/ui/explore/TrackCollector.cpp +++ b/src/lms/ui/explore/TrackCollector.cpp @@ -25,6 +25,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 "utils/Service.hpp" #include "Filters.hpp" @@ -37,7 +38,9 @@ namespace UserInterface RangeResults TrackCollector::get(Range range) { - Scrobbling::IScrobblingService& scrobbling {*Service::get()}; + Feedback::IFeedbackService& feedbackService{*Service::get()}; + Scrobbling::IScrobblingService& scrobblingService {*Service::get()}; + range = getActualRange(range); RangeResults tracks; @@ -48,15 +51,15 @@ namespace UserInterface break; case Mode::Starred: - tracks = scrobbling.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); + tracks = feedbackService.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case TrackCollector::Mode::RecentlyPlayed: - tracks = scrobbling.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); + tracks = scrobblingService.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::MostPlayed: - tracks = scrobbling.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); + tracks = scrobblingService.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range); break; case Mode::RecentlyAdded: diff --git a/src/lms/ui/explore/TrackListHelpers.cpp b/src/lms/ui/explore/TrackListHelpers.cpp index 8ba46ad0..4afae186 100644 --- a/src/lms/ui/explore/TrackListHelpers.cpp +++ b/src/lms/ui/explore/TrackListHelpers.cpp @@ -30,7 +30,7 @@ #include "services/database/Session.hpp" #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" -#include "services/scrobbling/IScrobblingService.hpp" +#include "services/feedback/IFeedbackService.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" @@ -217,7 +217,7 @@ namespace UserInterface::TrackListHelpers }); { - auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; + auto isStarred {[=] { return Service::get()->isStarred(LmsApp->getUserId(), trackId); }}; Wt::WPushButton* starBtn {entry->bindNew("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))}; starBtn->clicked().connect([=] @@ -226,12 +226,12 @@ namespace UserInterface::TrackListHelpers if (isStarred()) { - Service::get()->unstar(LmsApp->getUserId(), trackId); + Service::get()->unstar(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.star")); } else { - Service::get()->star(LmsApp->getUserId(), trackId); + Service::get()->star(LmsApp->getUserId(), trackId); starBtn->setText(Wt::WString::tr("Lms.Explore.unstar")); } });