Separated feedback services (stars / love for LB) from scrobbling services, to ease last.fm integration
This commit is contained in:
@@ -240,10 +240,14 @@
|
|||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
<message id="Lms.Settings.audio-settings-are-local">These audio settings are local to your browser!</message>
|
||||||
|
<message id="Lms.Settings.backend.internal">Internal</message>
|
||||||
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
|
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
|
||||||
<message id="Lms.Settings.change-password">Change password</message>
|
<message id="Lms.Settings.change-password">Change password</message>
|
||||||
<message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message>
|
<message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message>
|
||||||
<message id="Lms.Settings.default-transcode-format">Default transcode format</message>
|
<message id="Lms.Settings.default-transcode-format">Default transcode format</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
|
||||||
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
|
||||||
<message id="Lms.Settings.password-bad">Bad password</message>
|
<message id="Lms.Settings.password-bad">Bad password</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Old password must be filled in</message>
|
||||||
@@ -255,10 +259,7 @@
|
|||||||
<message id="Lms.Settings.replaygain-preamp">ReplayGain preamp</message>
|
<message id="Lms.Settings.replaygain-preamp">ReplayGain preamp</message>
|
||||||
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain preamp (if no info)</message>
|
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain preamp (if no info)</message>
|
||||||
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</message>
|
<message id="Lms.Settings.services">Services</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Internal</message>
|
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
|
||||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">ListenBrainz token</message>
|
|
||||||
<message id="Lms.Settings.settings">Settings</message>
|
<message id="Lms.Settings.settings">Settings</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Artist list mode</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">All artists</message>
|
||||||
|
|||||||
@@ -240,10 +240,14 @@
|
|||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
<message id="Lms.Settings.audio-settings-are-local">Ces paramètres audio sont locaux à votre navigateur !</message>
|
||||||
|
<message id="Lms.Settings.backend.internal">Interne</message>
|
||||||
|
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||||
|
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
|
||||||
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
<message id="Lms.Settings.change-password">Changement de mot de passe</message>
|
||||||
<message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message>
|
<message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message>
|
||||||
<message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message>
|
<message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message>
|
||||||
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
|
||||||
|
<message id="Lms.Settings.feedback">Feedback</message>
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
|
||||||
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
<message id="Lms.Settings.password-must-fill-old-password">L'ancien mot de passe doit être renseigné</message>
|
||||||
@@ -255,10 +259,7 @@
|
|||||||
<message id="Lms.Settings.replaygain-preamp">Pre-amplification ReplayGain</message>
|
<message id="Lms.Settings.replaygain-preamp">Pre-amplification ReplayGain</message>
|
||||||
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si pas d'info)</message>
|
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si pas d'info)</message>
|
||||||
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler">Scrobbler</message>
|
<message id="Lms.Settings.services">Services</message>
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.internal">Interne</message>
|
|
||||||
<message id="Lms.Settings.scrobbling.scrobbler.listenbrainz">ListenBrainz</message>
|
|
||||||
<message id="Lms.Settings.scrobbling.listenbrainz-token">Jeton ListenBrainz</message>
|
|
||||||
<message id="Lms.Settings.settings">Paramètres</message>
|
<message id="Lms.Settings.settings">Paramètres</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Mode de listage des artistes</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tous les artistes</message>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<message id="Lms.cancel">Annulla</message>
|
<message id="Lms.cancel">Annulla</message>
|
||||||
<message id="Lms.clusters">Tags</message>
|
<message id="Lms.clusters">Tags</message>
|
||||||
<message id="Lms.create">Crea</message>
|
<message id="Lms.create">Crea</message>
|
||||||
<message id="Lms.del">Elimina</message>
|
<message id="Lms.delete">Elimina</message>
|
||||||
<message id="Lms.discard">Annulla</message>
|
<message id="Lms.discard">Annulla</message>
|
||||||
<message id="Lms.edit">Modifica</message>
|
<message id="Lms.edit">Modifica</message>
|
||||||
<message id="Lms.loading">Caricamento...</message>
|
<message id="Lms.loading">Caricamento...</message>
|
||||||
@@ -240,10 +240,14 @@
|
|||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<message id="Lms.Settings.audio">Audio</message>
|
<message id="Lms.Settings.audio">Audio</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
<message id="Lms.Settings.audio-settings-are-local">Queste impostazioni sono salvate localmente su questo dispositivo!</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.change-password">Cambia password</message>
|
<message id="Lms.Settings.change-password">Cambia password</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
|
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
|
||||||
|
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
|
||||||
<message id="Lms.Settings.password-bad">Password errata</message>
|
<message id="Lms.Settings.password-bad">Password errata</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
|
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
|
||||||
@@ -257,9 +261,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">Modalità di elencazione artisti</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Tutti gli artisti</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
|
||||||
|
|||||||
@@ -240,10 +240,14 @@
|
|||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<message id="Lms.Settings.audio">音频</message>
|
<message id="Lms.Settings.audio">音频</message>
|
||||||
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器!</message>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.change-password">更改密码</message>
|
<message id="Lms.Settings.change-password">更改密码</message>
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
|
||||||
|
|
||||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
|
||||||
<message id="Lms.Settings.password-bad">无效密码</message>
|
<message id="Lms.Settings.password-bad">无效密码</message>
|
||||||
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
|
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
|
||||||
@@ -257,9 +261,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
|
||||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
|
||||||
|
|||||||
+17
-8
@@ -28,9 +28,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<label class="form-label" for="${id:transcode-bitrate}">
|
<label class="form-label" for="${id:transcode-bitrate}">
|
||||||
${tr:Lms.Settings.transcode-bitrate}
|
${tr:Lms.Settings.transcode-bitrate}
|
||||||
; </label>
|
</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
${transcode-bitrate class="form-control"}
|
${transcode-bitrate class="form-control"}
|
||||||
<span class="input-group-text">kbps</span>
|
<span class="input-group-text">kbps</span>
|
||||||
@@ -105,19 +105,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${</if-has-subsonic-api>}
|
${</if-has-subsonic-api>}
|
||||||
<legend>${tr:Lms.Settings.scrobbling}</legend>
|
<legend>${tr:Lms.Settings.services}</legend>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<label class="form-label" for="${id:scrobbler}">
|
<label class="form-label" for="${id:scrobbling-backend}">
|
||||||
${tr:Lms.Settings.scrobbling.scrobbler}
|
${tr:Lms.Settings.scrobbling}
|
||||||
</label>
|
</label>
|
||||||
${scrobbler class="form-control"}
|
${scrobbling-backend class="form-control"}
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
${scrobbler-info class="help-block"}
|
${scrobbling-backend-info class="help-block"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
|
<label class="form-label" for="${id:feedback-backend}">
|
||||||
|
${tr:Lms.Settings.feedback}
|
||||||
|
</label>
|
||||||
|
${feedback-backend class="form-control"}
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
${feedback-backend-info class="help-block"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-12">
|
||||||
<label class="form-label" for="${id:listenbrainz-token}">
|
<label class="form-label" for="${id:listenbrainz-token}">
|
||||||
${tr:Lms.Settings.scrobbling.listenbrainz-token}
|
${tr:Lms.Settings.backend.listenbrainz-token}
|
||||||
</label>
|
</label>
|
||||||
${listenbrainz-token class="form-control"}
|
${listenbrainz-token class="form-control"}
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
add_subdirectory(auth)
|
add_subdirectory(auth)
|
||||||
add_subdirectory(cover)
|
add_subdirectory(cover)
|
||||||
add_subdirectory(database)
|
add_subdirectory(database)
|
||||||
|
add_subdirectory(feedback)
|
||||||
add_subdirectory(recommendation)
|
add_subdirectory(recommendation)
|
||||||
add_subdirectory(scanner)
|
add_subdirectory(scanner)
|
||||||
add_subdirectory(scrobbling)
|
add_subdirectory(scrobbling)
|
||||||
|
|||||||
@@ -133,11 +133,11 @@ createQuery(Session& session, const Artist::FindParameters& params)
|
|||||||
|
|
||||||
if (params.starringUser.isValid())
|
if (params.starringUser.isValid())
|
||||||
{
|
{
|
||||||
assert(params.scrobbler);
|
assert(params.feedbackBackend);
|
||||||
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||||
.where("s_a.user_id = ?").bind(params.starringUser)
|
.where("s_a.user_id = ?").bind(params.starringUser)
|
||||||
.where("s_a.scrobbler = ?").bind(*params.scrobbler)
|
.where("s_a.backend = ?").bind(*params.feedbackBackend)
|
||||||
.where("s_a.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
.where("s_a.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.clusters.empty())
|
if (!params.clusters.empty())
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ namespace
|
|||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||||
{
|
{
|
||||||
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
|
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
|
||||||
.join("track t ON t.id = t_a_l.track_id")
|
.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("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
|
||||||
.join("listen l ON l.track_id = t.id")
|
.join("listen l ON l.track_id = t.id")
|
||||||
.where("l.user_id = ?").bind(userId)
|
.where("l.user_id = ?").bind(userId)
|
||||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
if (linkType)
|
if (linkType)
|
||||||
query.where("t_a_l.type = ?").bind(*linkType);
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
@@ -66,13 +66,13 @@ namespace
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||||
{
|
{
|
||||||
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
||||||
.join("track t ON t.release_id = r.id")
|
.join("track t ON t.release_id = r.id")
|
||||||
.join("listen l ON l.track_id = t.id")
|
.join("listen l ON l.track_id = t.id")
|
||||||
.where("l.user_id = ?").bind(userId)
|
.where("l.user_id = ?").bind(userId)
|
||||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (!clusterIds.empty())
|
||||||
{
|
{
|
||||||
@@ -98,12 +98,12 @@ namespace
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||||
{
|
{
|
||||||
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
||||||
.join("listen l ON l.track_id = t.id")
|
.join("listen l ON l.track_id = t.id")
|
||||||
.where("l.user_id = ?").bind(userId)
|
.where("l.user_id = ?").bind(userId)
|
||||||
.where("l.scrobbler = ?").bind(scrobbler) };
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
if (!clusterIds.empty())
|
||||||
{
|
{
|
||||||
@@ -131,16 +131,16 @@ namespace
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||||
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
||||||
, _scrobbler{ scrobbler }
|
, _backend{ backend }
|
||||||
, _user{ getDboPtr(user) }
|
, _user{ getDboPtr(user) }
|
||||||
, _track{ getDboPtr(track) }
|
, _track{ getDboPtr(track) }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||||
{
|
{
|
||||||
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, scrobbler, dateTime }});
|
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t Listen::getCount(Session& session)
|
std::size_t Listen::getCount(Session& session)
|
||||||
@@ -165,30 +165,30 @@ namespace Database
|
|||||||
if (parameters.user.isValid())
|
if (parameters.user.isValid())
|
||||||
query.where("user_id = ?").bind(parameters.user);
|
query.where("user_id = ?").bind(parameters.user);
|
||||||
|
|
||||||
if (parameters.scrobbler)
|
if (parameters.backend)
|
||||||
query.where("scrobbler = ?").bind(*parameters.scrobbler);
|
query.where("backend = ?").bind(*parameters.backend);
|
||||||
|
|
||||||
if (parameters.scrobblingState)
|
if (parameters.syncState)
|
||||||
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
|
query.where("sync_state = ?").bind(*parameters.syncState);
|
||||||
|
|
||||||
return Utils::execQuery(query, parameters.range);
|
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();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<Listen>()
|
return session.getDboSession().find<Listen>()
|
||||||
.where("user_id = ?").bind(userId)
|
.where("user_id = ?").bind(userId)
|
||||||
.where("track_id = ?").bind(trackId)
|
.where("track_id = ?").bind(trackId)
|
||||||
.where("scrobbler = ?").bind(scrobbler)
|
.where("backend = ?").bind(backend)
|
||||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
{
|
{
|
||||||
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
|
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) };
|
||||||
|
|
||||||
auto collection{ query
|
auto collection{ query
|
||||||
.orderBy("COUNT(a.id) DESC")
|
.orderBy("COUNT(a.id) DESC")
|
||||||
@@ -197,72 +197,72 @@ namespace Database
|
|||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
{
|
{
|
||||||
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||||
.orderBy("COUNT(r.id) DESC")
|
.orderBy("COUNT(r.id) DESC")
|
||||||
.groupBy("r.id") };
|
.groupBy("r.id") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
{
|
{
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||||
.orderBy("COUNT(t.id) DESC")
|
.orderBy("COUNT(t.id) DESC")
|
||||||
.groupBy("t.id") };
|
.groupBy("t.id") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> 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)")
|
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||||
.orderBy("l.date_time DESC") };
|
.orderBy("l.date_time DESC") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& 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)")
|
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||||
.orderBy("l.date_time DESC") };
|
.orderBy("l.date_time DESC") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
|
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& 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)")
|
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||||
.orderBy("l.date_time DESC") };
|
.orderBy("l.date_time DESC") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
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?
|
// TODO not pending remove?
|
||||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||||
.join("track t ON l.track_id = t.id")
|
.join("track t ON l.track_id = t.id")
|
||||||
.where("t.release_id = ?").bind(releaseId)
|
.where("t.release_id = ?").bind(releaseId)
|
||||||
.where("l.user_id = ?").bind(userId)
|
.where("l.user_id = ?").bind(userId)
|
||||||
.where("l.scrobbler = ?").bind(scrobbler)
|
.where("l.backend = ?").bind(backend)
|
||||||
.orderBy("l.date_time DESC")
|
.orderBy("l.date_time DESC")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.resultValue();
|
.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?
|
// TODO not pending remove?
|
||||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||||
.join("track t ON track_id = t.id")
|
.join("track t ON track_id = t.id")
|
||||||
.where("t.id = ?").bind(trackId)
|
.where("t.id = ?").bind(trackId)
|
||||||
.where("l.user_id = ?").bind(userId)
|
.where("l.user_id = ?").bind(userId)
|
||||||
.where("l.scrobbler = ?").bind(scrobbler)
|
.where("l.backend = ?").bind(backend)
|
||||||
.orderBy("l.date_time DESC")
|
.orderBy("l.date_time DESC")
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.resultValue();
|
.resultValue();
|
||||||
|
|||||||
@@ -72,414 +72,6 @@ namespace Database::Migration
|
|||||||
Db& _db;
|
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<int>(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<int>(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<int>(User::defaultSubsonicTranscodeFormat))
|
|
||||||
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
|
|
||||||
+ ", " + std::to_string(static_cast<int>(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<int>(/*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<int>(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<IdType::ValueType, Scrobbler> userScrobblers;
|
|
||||||
auto getScrobbler{ [&](IdType::ValueType userId)
|
|
||||||
{
|
|
||||||
auto itScrobbler {userScrobblers.find(userId)};
|
|
||||||
if (itScrobbler != std::cend(userScrobblers))
|
|
||||||
return itScrobbler->second;
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<Scrobbler>("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<IdType::ValueType /* userId */, IdType::ValueType /* entryId */>;
|
|
||||||
|
|
||||||
std::vector<UserIdObjectId> starredEntries;
|
|
||||||
auto query {session.getDboSession().query<UserIdObjectId>("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<IdType::ValueType /* userId */, IdType::ValueType /* trackId */, Wt::WDateTime>;
|
|
||||||
|
|
||||||
std::vector<UserIdObjectId> listens;
|
|
||||||
auto query {session.getDboSession().query<UserIdObjectId>("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)
|
static void migrateFromV32(Session& session)
|
||||||
{
|
{
|
||||||
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
|
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");
|
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)
|
void doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
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<unsigned, MigrationFunction> migrationFunctions
|
const std::map<unsigned, MigrationFunction> 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},
|
{32, migrateFromV32},
|
||||||
{33, migrateFromV33},
|
{33, migrateFromV33},
|
||||||
{34, migrateFromV34},
|
{34, migrateFromV34},
|
||||||
@@ -652,6 +244,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
{39, migrateFromV39},
|
{39, migrateFromV39},
|
||||||
{40, migrateFromV40},
|
{40, migrateFromV40},
|
||||||
{41, migrateFromV41},
|
{41, migrateFromV41},
|
||||||
|
{42, migrateFromV42},
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Database
|
|||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 42 };
|
static constexpr Version LMS_DATABASE_VERSION{ 43 };
|
||||||
class VersionInfo
|
class VersionInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -64,11 +64,11 @@ namespace Database
|
|||||||
|
|
||||||
if (params.starringUser.isValid())
|
if (params.starringUser.isValid())
|
||||||
{
|
{
|
||||||
assert(params.scrobbler);
|
assert(params.feedbackBackend);
|
||||||
query.join("starred_release s_r ON s_r.release_id = r.id")
|
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||||
.where("s_r.user_id = ?").bind(params.starringUser)
|
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||||
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
|
.where("s_r.backend = ?").bind(*params.feedbackBackend)
|
||||||
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.artist.isValid())
|
if (params.artist.isValid())
|
||||||
|
|||||||
@@ -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_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_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 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_backend_idx ON listen(backend)");
|
||||||
_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_backend_idx ON listen(user_id,backend)");
|
||||||
_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 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_scrobbler_idx ON starred_artist(user_id,scrobbler)");
|
_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_scrobbler_idx ON starred_artist(artist_id,user_id,scrobbler)");
|
_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_scrobbler_idx ON starred_release(user_id,scrobbler)");
|
_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_scrobbler_idx ON starred_release(release_id,user_id,scrobbler)");
|
_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_scrobbler_idx ON starred_track(user_id,scrobbler)");
|
_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_scrobbler_idx ON starred_track(track_id,user_id,scrobbler)");
|
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial settings tables
|
// Initial settings tables
|
||||||
|
|||||||
@@ -29,47 +29,42 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
: _scrobbler {scrobbler}
|
: _backend{ backend }
|
||||||
, _artist {getDboPtr(artist)}
|
, _artist{ getDboPtr(artist) }
|
||||||
, _user {getDboPtr(user)}
|
, _user{ getDboPtr(user) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
StarredArtist::pointer
|
StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
{
|
||||||
{
|
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
|
||||||
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist {artist, user, scrobbler}});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t StarredArtist::getCount(Session& session)
|
||||||
StarredArtist::getCount(Session& session)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredArtist::pointer
|
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
|
||||||
StarredArtist::find(Session& session, StarredArtistId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredArtist::pointer
|
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
|
||||||
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredArtist>()
|
||||||
return session.getDboSession().find<StarredArtist>()
|
.where("artist_id = ?").bind(artistId)
|
||||||
.where("artist_id = ?").bind(artistId)
|
.where("user_id = ?").bind(userId)
|
||||||
.where("user_id = ?").bind(userId)
|
.where("backend = ?").bind(backend)
|
||||||
.where("scrobbler = ?").bind(scrobbler)
|
.resultValue();
|
||||||
.resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
{
|
||||||
{
|
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,47 +29,42 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
: _scrobbler {scrobbler}
|
: _backend{ backend }
|
||||||
, _release {getDboPtr(release)}
|
, _release{ getDboPtr(release) }
|
||||||
, _user {getDboPtr(user)}
|
, _user{ getDboPtr(user) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
StarredRelease::pointer
|
StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
{
|
||||||
{
|
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
|
||||||
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{release, user, scrobbler}});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t StarredRelease::getCount(Session& session)
|
||||||
StarredRelease::getCount(Session& session)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredRelease::pointer
|
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
|
||||||
StarredRelease::find(Session& session, StarredReleaseId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredRelease::pointer
|
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
|
||||||
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredRelease>()
|
||||||
return session.getDboSession().find<StarredRelease>()
|
.where("release_id = ?").bind(releaseId)
|
||||||
.where("release_id = ?").bind(releaseId)
|
.where("user_id = ?").bind(userId)
|
||||||
.where("user_id = ?").bind(userId)
|
.where("backend = ?").bind(backend)
|
||||||
.where("scrobbler = ?").bind(scrobbler)
|
.resultValue();
|
||||||
.resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
{
|
||||||
{
|
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,64 +29,58 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
: _scrobbler {scrobbler}
|
: _backend{ backend }
|
||||||
, _track {getDboPtr(track)}
|
, _track{ getDboPtr(track) }
|
||||||
, _user {getDboPtr(user)}
|
, _user{ getDboPtr(user) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
StarredTrack::pointer
|
StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
|
||||||
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
{
|
||||||
{
|
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
|
||||||
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{track, user, scrobbler}});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t StarredTrack::getCount(Session& session)
|
||||||
StarredTrack::getCount(Session& session)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredTrack::pointer
|
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
|
||||||
StarredTrack::find(Session& session, StarredTrackId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
StarredTrack::pointer
|
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||||
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<StarredTrack>()
|
||||||
return session.getDboSession().find<StarredTrack>()
|
.where("track_id = ?").bind(trackId)
|
||||||
.where("track_id = ?").bind(trackId)
|
.where("user_id = ?").bind(userId)
|
||||||
.where("user_id = ?").bind(userId)
|
.where("backend = ?").bind(backend)
|
||||||
.where("scrobbler = ?").bind(scrobbler)
|
.resultValue();
|
||||||
.resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
RangeResults<StarredTrackId>
|
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
|
||||||
StarredTrack::find(Session& session, const FindParameters& params)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t")};
|
auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
|
||||||
|
|
||||||
if (params.scrobbler)
|
if (params.backend)
|
||||||
query.where("s_t.scrobbler = ?").bind(*params.scrobbler);
|
query.where("s_t.backend = ?").bind(*params.backend);
|
||||||
if (params.scrobblingState)
|
if (params.syncState)
|
||||||
query.where("s_t.scrobbling_state = ?").bind(*params.scrobblingState);
|
query.where("s_t.sync_state = ?").bind(*params.syncState);
|
||||||
if (params.user.isValid())
|
if (params.user.isValid())
|
||||||
query.where("s_t.user_id = ?").bind(params.user);
|
query.where("s_t.user_id = ?").bind(params.user);
|
||||||
|
|
||||||
return Utils::execQuery(query, params.range);
|
return Utils::execQuery(query, params.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
||||||
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
{
|
||||||
{
|
_dateTime = Utils::normalizeDateTime(dateTime);
|
||||||
_dateTime = Utils::normalizeDateTime(dateTime);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ createQuery(Session& session, const Track::FindParameters& params)
|
|||||||
|
|
||||||
if (params.starringUser.isValid())
|
if (params.starringUser.isValid())
|
||||||
{
|
{
|
||||||
assert(params.scrobbler);
|
assert(params.feedbackBackend);
|
||||||
query.join("starred_track s_t ON s_t.track_id = t.id")
|
query.join("starred_track s_t ON s_t.track_id = t.id")
|
||||||
.where("s_t.user_id = ?").bind(params.starringUser)
|
.where("s_t.user_id = ?").bind(params.starringUser)
|
||||||
.where("s_t.scrobbler = ?").bind(*params.scrobbler)
|
.where("s_t.backend = ?").bind(*params.feedbackBackend)
|
||||||
.where("s_t.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
.where("s_t.sync_state <> ?").bind(SyncState::PendingRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.clusters.empty())
|
if (!params.clusters.empty())
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ namespace Database {
|
|||||||
|
|
||||||
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
|
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
|
||||||
|
|
||||||
if (params.scrobbler)
|
if (params.scrobblingBackend)
|
||||||
query.where("scrobbler = ?").bind(*params.scrobbler);
|
query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
|
||||||
|
if (params.feedbackBackend)
|
||||||
|
query.where("feedback_backend = ?").bind(*params.feedbackBackend);
|
||||||
|
|
||||||
return Utils::execQuery(query, params.range);
|
return Utils::execQuery(query, params.range);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Artist final : public Object<Artist, ArtistId>
|
|||||||
Range range;
|
Range range;
|
||||||
Wt::WDateTime writtenAfter;
|
Wt::WDateTime writtenAfter;
|
||||||
UserId starringUser; // only artists starred by this user
|
UserId starringUser; // only artists starred by this user
|
||||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||||
TrackId track; // artists involved in this track
|
TrackId track; // artists involved in this track
|
||||||
ReleaseId release; // artists involved in this release
|
ReleaseId release; // artists involved in this release
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class Artist final : public Object<Artist, ArtistId>
|
|||||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; 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& setTrack(TrackId _track) { track = _track; return *this; }
|
||||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,47 +46,47 @@ namespace Database
|
|||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
UserId user;
|
UserId user;
|
||||||
std::optional<Scrobbler> scrobbler;
|
std::optional<ScrobblingBackend> backend;
|
||||||
std::optional<ScrobblingState> scrobblingState;
|
std::optional<SyncState> syncState;
|
||||||
Range range;
|
Range range;
|
||||||
|
|
||||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||||
FindParameters& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
|
FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
|
||||||
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; }
|
FindParameters& setSyncState(SyncState _syncState) { syncState = _syncState; return *this; }
|
||||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, ListenId id);
|
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<ListenId> find(Session& session, const FindParameters& parameters);
|
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
||||||
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
|
|
||||||
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
|
||||||
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
|
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {});
|
||||||
|
|
||||||
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
|
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId);
|
||||||
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
|
static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId);
|
||||||
|
|
||||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
SyncState getSyncState() const { return _syncState; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
|
|
||||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
void setSyncState(SyncState state) { _syncState = state; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _backend, "backend"); // TODO rename
|
||||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
Wt::Dbo::field(a, _syncState, "sync_state"); // TODO rename
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
@@ -94,12 +94,12 @@ namespace Database
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||||
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
|
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime);
|
||||||
|
|
||||||
Wt::WDateTime _dateTime;
|
Wt::WDateTime _dateTime;
|
||||||
Scrobbler _scrobbler;
|
ScrobblingBackend _backend;
|
||||||
ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd };
|
SyncState _syncState{ SyncState::PendingAdd };
|
||||||
|
|
||||||
Wt::Dbo::ptr<User> _user;
|
Wt::Dbo::ptr<User> _user;
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Database
|
|||||||
Wt::WDateTime writtenAfter;
|
Wt::WDateTime writtenAfter;
|
||||||
std::optional<DateRange> dateRange;
|
std::optional<DateRange> dateRange;
|
||||||
UserId starringUser; // only releases starred by this user
|
UserId starringUser; // only releases starred by this user
|
||||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
std::optional<FeedbackBackend> feedbackBackend; // and for this backend
|
||||||
ArtistId artist; // only releases that involved this user
|
ArtistId artist; // only releases that involved this user
|
||||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||||
@@ -70,7 +70,7 @@ namespace Database
|
|||||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
|
FindParameters& setDateRange(const std::optional<DateRange>& _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<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||||
{
|
{
|
||||||
artist = _artist;
|
artist = _artist;
|
||||||
@@ -104,7 +104,7 @@ namespace Database
|
|||||||
std::optional<std::string> getCopyrightURL() const;
|
std::optional<std::string> getCopyrightURL() const;
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||||
|
|||||||
@@ -30,53 +30,53 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
class Session;
|
class Session;
|
||||||
class User;
|
class User;
|
||||||
|
|
||||||
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
|
class StarredArtist final : public Object<StarredArtist, StarredArtistId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
StarredArtist() = default;
|
StarredArtist() = default;
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, StarredArtistId id);
|
static pointer find(Session& session, StarredArtistId id);
|
||||||
static pointer find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler);
|
static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
SyncState getSyncState() const { return _syncState; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setDateTime(const Wt::WDateTime& dateTime);
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
void setSyncState(SyncState state) { _syncState = state; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _backend, "backend");
|
||||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||||
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler);
|
static pointer create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend scrobblingbackend);
|
||||||
|
|
||||||
Scrobbler _scrobbler; // for which scrobbler
|
FeedbackBackend _backend; // for which backend
|
||||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
SyncState _syncState{ SyncState::PendingAdd };
|
||||||
Wt::WDateTime _dateTime; // when it was starred
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
Wt::Dbo::ptr<Artist> _artist;
|
Wt::Dbo::ptr<Artist> _artist;
|
||||||
Wt::Dbo::ptr<User> _user;
|
Wt::Dbo::ptr<User> _user;
|
||||||
};
|
};
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -30,53 +30,53 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class User;
|
class User;
|
||||||
|
|
||||||
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
|
class StarredRelease final : public Object<StarredRelease, StarredReleaseId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
StarredRelease() = default;
|
StarredRelease() = default;
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, StarredReleaseId id);
|
static pointer find(Session& session, StarredReleaseId id);
|
||||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler);
|
static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
ObjectPtr<Release> getRelease() const { return _release; }
|
ObjectPtr<Release> getRelease() const { return _release; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
FeedbackBackend getFeedbackBackend() const { return _backend; }
|
||||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
SyncState getSyncState() const { return _syncState; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setDateTime(const Wt::WDateTime& dateTime);
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
void setSyncState(SyncState state) { _syncState = state; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _backend, "backend");
|
||||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||||
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler);
|
static pointer create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend);
|
||||||
|
|
||||||
Scrobbler _scrobbler; // for which scrobbler
|
FeedbackBackend _backend; // for which backend
|
||||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
SyncState _syncState{ SyncState::PendingAdd };
|
||||||
Wt::WDateTime _dateTime; // when it was starred
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::ptr<User> _user;
|
Wt::Dbo::ptr<User> _user;
|
||||||
};
|
};
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -31,66 +31,66 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Track;
|
class Track;
|
||||||
class Session;
|
class Session;
|
||||||
class User;
|
class User;
|
||||||
|
|
||||||
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
|
class StarredTrack final : public Object<StarredTrack, StarredTrackId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
StarredTrack() = default;
|
StarredTrack() = default;
|
||||||
|
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::optional<Scrobbler> scrobbler; // for this scrobbler
|
std::optional<FeedbackBackend> backend; // for this backend
|
||||||
std::optional<ScrobblingState> scrobblingState; // and these states
|
std::optional<SyncState> syncState; // and these states
|
||||||
UserId user; // and this user
|
UserId user; // and this user
|
||||||
Range range;
|
Range range;
|
||||||
|
|
||||||
FindParameters& setScrobbler(Scrobbler _scrobbler, ScrobblingState _scrobblingState) { scrobbler = _scrobbler; scrobblingState = _scrobblingState; return *this; }
|
FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
|
||||||
FindParameters& setUser(UserId _user) {user = _user; return *this; }
|
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, StarredTrackId id);
|
static pointer find(Session& session, StarredTrackId id);
|
||||||
static pointer find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler);
|
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
FeedbackBackend getBackend() const { return _backend; }
|
||||||
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
const Wt::WDateTime& getDateTime() const { return _dateTime; }
|
||||||
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
SyncState getSyncState() const { return _syncState; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setDateTime(const Wt::WDateTime& dateTime);
|
void setDateTime(const Wt::WDateTime& dateTime);
|
||||||
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
|
void setSyncState(SyncState state) { _syncState = state; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _scrobbler, "scrobbler");
|
Wt::Dbo::field(a, _backend, "backend");
|
||||||
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
|
Wt::Dbo::field(a, _syncState, "sync_state");
|
||||||
Wt::Dbo::field(a, _dateTime, "date_time");
|
Wt::Dbo::field(a, _dateTime, "date_time");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler);
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend);
|
||||||
|
|
||||||
Scrobbler _scrobbler; // for which scrobbler
|
FeedbackBackend _backend; // for which backend
|
||||||
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd};
|
SyncState _syncState{ SyncState::PendingAdd };
|
||||||
Wt::WDateTime _dateTime; // when it was starred
|
Wt::WDateTime _dateTime; // when it was starred
|
||||||
|
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
Wt::Dbo::ptr<User> _user;
|
Wt::Dbo::ptr<User> _user;
|
||||||
};
|
};
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -61,23 +61,23 @@ class Track final : public Object<Track, TrackId>
|
|||||||
public:
|
public:
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
|
||||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
std::string name; // if non empty, must match this name
|
std::string name; // if non empty, must match this name
|
||||||
TrackSortMethod sortMethod {TrackSortMethod::None};
|
TrackSortMethod sortMethod {TrackSortMethod::None};
|
||||||
Range range;
|
Range range;
|
||||||
Wt::WDateTime writtenAfter;
|
Wt::WDateTime writtenAfter;
|
||||||
UserId starringUser; // only tracks starred by this user
|
UserId starringUser; // only tracks starred by this user
|
||||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
|
||||||
ArtistId artist; // only tracks that involve this artist
|
ArtistId artist; // only tracks that involve this artist
|
||||||
std::string artistName; // only tracks that involve this artist name
|
std::string artistName; // only tracks that involve this artist name
|
||||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||||
bool nonRelease {}; // only tracks that do not belong to a release
|
bool nonRelease {}; // only tracks that do not belong to a release
|
||||||
ReleaseId release; // matching this release
|
ReleaseId release; // matching this release
|
||||||
std::string releaseName; // matching this release name
|
std::string releaseName; // matching this release name
|
||||||
TrackListId trackList; // matching this trackList
|
TrackListId trackList; // matching this trackList
|
||||||
std::optional<int> trackNumber; // matching this track number
|
std::optional<int> trackNumber; // matching this track number
|
||||||
bool distinct {true};
|
bool distinct {true};
|
||||||
|
|
||||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||||
@@ -85,7 +85,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
|
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
|
||||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; 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<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
||||||
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
|
||||||
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
|
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
|
||||||
|
|||||||
@@ -160,13 +160,19 @@ namespace Database
|
|||||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
|
||||||
bool isAudioBitrateAllowed(Bitrate bitrate);
|
bool isAudioBitrateAllowed(Bitrate bitrate);
|
||||||
|
|
||||||
enum class Scrobbler
|
enum class ScrobblingBackend
|
||||||
{
|
{
|
||||||
Internal = 0,
|
Internal = 0,
|
||||||
ListenBrainz = 1,
|
ListenBrainz = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ScrobblingState
|
enum class FeedbackBackend
|
||||||
|
{
|
||||||
|
Internal = 0,
|
||||||
|
ListenBrainz = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SyncState
|
||||||
{
|
{
|
||||||
PendingAdd = 0,
|
PendingAdd = 0,
|
||||||
Synchronized = 1,
|
Synchronized = 1,
|
||||||
|
|||||||
@@ -47,20 +47,23 @@ namespace Database {
|
|||||||
|
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::optional<Scrobbler> scrobbler;
|
std::optional<ScrobblingBackend> scrobblingBackend;
|
||||||
Range range;
|
std::optional<FeedbackBackend> 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; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline const std::size_t MinNameLength{ 3 };
|
static inline constexpr std::size_t MinNameLength{ 3 };
|
||||||
static inline const std::size_t MaxNameLength{ 15 };
|
static inline constexpr std::size_t MaxNameLength{ 15 };
|
||||||
static inline const AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
|
static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS };
|
||||||
static inline const Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 };
|
||||||
static inline const UITheme defaultUITheme{ UITheme::Dark };
|
static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
|
||||||
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
|
||||||
static inline const Scrobbler defaultScrobbler{ Scrobbler::Internal };
|
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
|
||||||
|
static inline constexpr FeedbackBackend defaultFeedbackBackend{ FeedbackBackend::Internal };
|
||||||
|
|
||||||
User() = default;
|
User() = default;
|
||||||
|
|
||||||
@@ -88,7 +91,8 @@ namespace Database {
|
|||||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||||
void clearAuthTokens();
|
void clearAuthTokens();
|
||||||
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
|
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<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
|
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
|
||||||
|
|
||||||
// read
|
// read
|
||||||
@@ -102,7 +106,8 @@ namespace Database {
|
|||||||
bool isRadioSet() const { return _radio; }
|
bool isRadioSet() const { return _radio; }
|
||||||
UITheme getUITheme() const { return _uiTheme; }
|
UITheme getUITheme() const { return _uiTheme; }
|
||||||
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
||||||
Scrobbler getScrobbler() const { return _scrobbler; }
|
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
|
||||||
|
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
|
||||||
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
@@ -117,7 +122,8 @@ namespace Database {
|
|||||||
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
|
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate");
|
||||||
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
|
||||||
Wt::Dbo::field(a, _uiTheme, "ui_theme");
|
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");
|
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
|
||||||
|
|
||||||
// UI player settings
|
// UI player settings
|
||||||
@@ -138,7 +144,8 @@ namespace Database {
|
|||||||
std::string _passwordHash;
|
std::string _passwordHash;
|
||||||
Wt::WDateTime _lastLogin;
|
Wt::WDateTime _lastLogin;
|
||||||
UITheme _uiTheme{ defaultUITheme };
|
UITheme _uiTheme{ defaultUITheme };
|
||||||
Scrobbler _scrobbler{ defaultScrobbler };
|
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
|
||||||
|
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
|
||||||
std::string _listenbrainzToken; // Musicbrainz Identifier
|
std::string _listenbrainzToken; // Musicbrainz Identifier
|
||||||
|
|
||||||
// Admin defined settings
|
// Admin defined settings
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
|||||||
EXPECT_EQ(Listen::getCount(session), 0);
|
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() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
@@ -45,7 +45,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createUniqueTransaction() };
|
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);
|
EXPECT_EQ(Listen::getCount(session), 2);
|
||||||
}
|
}
|
||||||
@@ -61,12 +61,12 @@ TEST_F(DatabaseFixture, Listen_get)
|
|||||||
{
|
{
|
||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
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 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);
|
EXPECT_EQ(listens.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,18 +74,18 @@ TEST_F(DatabaseFixture, Listen_get)
|
|||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(listens.moreResults, false);
|
||||||
ASSERT_EQ(listens.results.size(), 1);
|
ASSERT_EQ(listens.results.size(), 1);
|
||||||
EXPECT_EQ(listens.results.front(), listen->getId());
|
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);
|
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);
|
EXPECT_EQ(listens.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,14 +95,14 @@ TEST_F(DatabaseFixture, Listen_get_multi)
|
|||||||
{
|
{
|
||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
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 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(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
|
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(), Scrobbler::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}} };
|
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 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);
|
ASSERT_EQ(listens.results.size(), 3);
|
||||||
EXPECT_EQ(listens.results[0], listen1.getId());
|
EXPECT_EQ(listens.results[0], listen1.getId());
|
||||||
EXPECT_EQ(listens.results[1], listen2.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} };
|
const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} };
|
||||||
ASSERT_GT(dateTime2, dateTime1);
|
ASSERT_GT(dateTime2, dateTime1);
|
||||||
|
|
||||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||||
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 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);
|
ASSERT_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getId(), listen1.getId());
|
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);
|
ASSERT_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getId(), listen2.getId());
|
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);
|
EXPECT_FALSE(listen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,19 +148,19 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
|||||||
ScopedTrack track1{ session, "MyTrack" };
|
ScopedTrack track1{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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 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.results.size(), 0);
|
||||||
EXPECT_EQ(artists.moreResults, false);
|
EXPECT_EQ(artists.moreResults, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedTrack track2{ session, "MyTrack2" };
|
ScopedTrack track2{ session, "MyTrack2" };
|
||||||
ScopedArtist artist1{ session, "MyArtist1" };
|
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() };
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
@@ -171,20 +171,20 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -194,7 +194,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,24 +218,24 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.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 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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
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) };
|
||||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), Scrobbler::Internal, dateTime.addSecs(3) };
|
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(3) };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
EXPECT_EQ(artists.results[1], artist1->getId());
|
EXPECT_EQ(artists.results[1], artist1->getId());
|
||||||
@@ -243,7 +243,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.moreResults, true);
|
EXPECT_EQ(artists.moreResults, true);
|
||||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
@@ -256,7 +256,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
|||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedArtist artist{ session, "MyArtist" };
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -278,7 +278,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist.getId());
|
EXPECT_EQ(artists.results[0], artist.getId());
|
||||||
}
|
}
|
||||||
@@ -298,17 +298,17 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
EXPECT_EQ(releases.results.size(), 0);
|
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 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.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release.getId());
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
@@ -316,7 +316,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
EXPECT_EQ(releases.results.size(), 0);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
|||||||
ScopedTrack track2{ session, "MyTrack" };
|
ScopedTrack track2{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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 release1{ session, "MyRelease1" };
|
||||||
ScopedRelease release2{ session, "MyRelease2" };
|
ScopedRelease release2{ session, "MyRelease2" };
|
||||||
|
|
||||||
@@ -341,28 +341,28 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release1.getId());
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
}
|
}
|
||||||
ScopedListen listen2{ 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(), Scrobbler::Internal, dateTime };
|
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases.results[0], release2.getId());
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
EXPECT_EQ(releases.results[1], release1.getId());
|
EXPECT_EQ(releases.results[1], release1.getId());
|
||||||
}
|
}
|
||||||
ScopedListen listen4{ 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(), Scrobbler::Internal, dateTime };
|
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases.results[0], release1.getId());
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
@@ -375,7 +375,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
|||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
ScopedRelease release{ session, "MyRelease" };
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
@@ -388,7 +388,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -399,7 +399,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release.getId());
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
}
|
}
|
||||||
@@ -414,17 +414,17 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 0);
|
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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track.getId());
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
@@ -432,7 +432,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
EXPECT_EQ(tracks.results.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
@@ -444,33 +444,33 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
|||||||
ScopedTrack track2{ session, "MyTrack2" };
|
ScopedTrack track2{ session, "MyTrack2" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track1.getId());
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
}
|
}
|
||||||
ScopedListen listen2{ 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(), Scrobbler::Internal, dateTime };
|
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
EXPECT_EQ(tracks.results[1], track1.getId());
|
EXPECT_EQ(tracks.results[1], track1.getId());
|
||||||
}
|
}
|
||||||
ScopedListen listen4{ 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(), Scrobbler::Internal, dateTime };
|
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks.results[0], track1.getId());
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
@@ -483,14 +483,14 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
|||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -501,7 +501,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track.getId());
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
}
|
}
|
||||||
@@ -521,31 +521,31 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.results.size(), 0);
|
||||||
EXPECT_EQ(artists.moreResults, false);
|
EXPECT_EQ(artists.moreResults, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist->getId());
|
EXPECT_EQ(artists.results[0], artist->getId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -555,7 +555,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -579,32 +579,32 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.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 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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
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 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);
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
EXPECT_EQ(artists.results[1], artist1->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 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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.moreResults, true);
|
EXPECT_EQ(artists.moreResults, true);
|
||||||
EXPECT_EQ(artists.results[0], artist2->getId());
|
EXPECT_EQ(artists.results[0], artist2->getId());
|
||||||
@@ -617,7 +617,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
|||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedArtist artist{ session, "MyArtist" };
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
|
||||||
@@ -629,7 +629,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -639,7 +639,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
ASSERT_EQ(artists.results.size(), 1);
|
||||||
EXPECT_EQ(artists.results[0], artist.getId());
|
EXPECT_EQ(artists.results[0], artist.getId());
|
||||||
}
|
}
|
||||||
@@ -659,18 +659,18 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 0);
|
ASSERT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release.getId());
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
@@ -678,7 +678,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
EXPECT_EQ(releases.results.size(), 0);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
@@ -699,39 +699,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_FALSE(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
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} };
|
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 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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 1);
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release2.getId());
|
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 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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases.results[0], release1.getId());
|
EXPECT_EQ(releases.results[0], release1.getId());
|
||||||
EXPECT_EQ(releases.results[1], release2.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 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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases.results[0], release2.getId());
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
EXPECT_EQ(releases.results[1], release1.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 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);
|
EXPECT_EQ(releases.moreResults, false);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
EXPECT_EQ(releases.results[0], release2.getId());
|
EXPECT_EQ(releases.results[0], release2.getId());
|
||||||
@@ -812,17 +812,17 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
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 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);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,7 +833,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.size(), 1);
|
||||||
EXPECT_EQ(releases.results[0], release.getId());
|
EXPECT_EQ(releases.results[0], release.getId());
|
||||||
}
|
}
|
||||||
@@ -847,18 +847,18 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 0);
|
ASSERT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track.getId());
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
@@ -867,7 +867,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.moreResults, false);
|
||||||
EXPECT_EQ(tracks.results.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
@@ -881,39 +881,39 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_FALSE(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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_TRUE(listen);
|
||||||
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
||||||
}
|
}
|
||||||
@@ -926,44 +926,44 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
|||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 1);
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks.results[0], track1.getId());
|
EXPECT_EQ(tracks.results[0], track1.getId());
|
||||||
EXPECT_EQ(tracks.results[1], track2.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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
EXPECT_EQ(tracks.results[1], track1.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 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);
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
EXPECT_EQ(tracks.results[0], track2.getId());
|
EXPECT_EQ(tracks.results[0], track2.getId());
|
||||||
@@ -976,14 +976,14 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
|||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
ScopedUser user{ session, "MyUser" };
|
ScopedUser user{ session, "MyUser" };
|
||||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
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" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -994,7 +994,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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.size(), 1);
|
||||||
EXPECT_EQ(tracks.results[0], track.getId());
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,102 +26,100 @@ using ScopedStarredArtist = ScopedEntity<Database::StarredArtist>;
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredArtist)
|
TEST_F(DatabaseFixture, StarredArtist)
|
||||||
{
|
{
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedUser user2 {session, "MyUser2"};
|
ScopedUser user2{ session, "MyUser2" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto starredArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||||
EXPECT_FALSE(starredArtist);
|
EXPECT_FALSE(starredArtist);
|
||||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
||||||
|
|
||||||
auto artists {Artist::find(session, Artist::FindParameters {})};
|
auto artists{ Artist::find(session, Artist::FindParameters {}) };
|
||||||
EXPECT_EQ(artists.results.size(), 1);
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto gotArtist {StarredArtist::find(session, artist->getId(), user->getId(), Scrobbler::Internal)};
|
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||||
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
EXPECT_EQ(StarredArtist::getCount(session), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto artists {Artist::find(session, Artist::FindParameters {})};
|
auto artists{ Artist::find(session, Artist::FindParameters {}) };
|
||||||
EXPECT_EQ(artists.results.size(), 1);
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
|
||||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
artists = Artist::find(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(artists.results.size(), 1);
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
|
||||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
artists = Artist::find(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(artists.results.size(), 0);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||||
{
|
{
|
||||||
ScopedArtist artist {session, "MyArtist"};
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedStarredArtist starredArtist {session, artist.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
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))};
|
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||||
EXPECT_EQ(artists.results.size(), 1);
|
EXPECT_EQ(artists.results.size(), 1);
|
||||||
|
|
||||||
starredArtist.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
starredArtist.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||||
artists = Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
artists = Artist::find(session, Artist::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(artists.results.size(), 0);
|
EXPECT_EQ(artists.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||||
{
|
{
|
||||||
ScopedArtist artist1 {session, "MyArtist1"};
|
ScopedArtist artist1{ session, "MyArtist1" };
|
||||||
ScopedArtist artist2 {session, "MyArtist2"};
|
ScopedArtist artist2{ session, "MyArtist2" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
|
||||||
ScopedStarredArtist starredArtist1 {session, artist1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredArtist starredArtist1{ session, artist1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||||
ScopedStarredArtist starredArtist2 {session, artist2.lockAndGet(), user.lockAndGet(), Scrobbler::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))};
|
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||||
EXPECT_EQ(artists.results.size(), 2);
|
EXPECT_EQ(artists.results.size(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
|
|
||||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||||
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
ASSERT_EQ(artists.results.size(), 2);
|
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
||||||
EXPECT_EQ(artists.results[0], starredArtist1->getArtist()->getId());
|
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||||
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
}
|
||||||
}
|
{
|
||||||
{
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
auto artists {Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ArtistSortMethod::StarredDateDesc)) };
|
||||||
.setSortMethod(ArtistSortMethod::StarredDateDesc))};
|
ASSERT_EQ(artists.results.size(), 2);
|
||||||
ASSERT_EQ(artists.results.size(), 2);
|
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
||||||
EXPECT_EQ(artists.results[0], starredArtist2->getArtist()->getId());
|
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
||||||
EXPECT_EQ(artists.results[1], starredArtist1->getArtist()->getId());
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,102 +26,100 @@ using ScopedStarredRelease = ScopedEntity<Database::StarredRelease>;
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredRelease)
|
TEST_F(DatabaseFixture, StarredRelease)
|
||||||
{
|
{
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedUser user2 {session, "MyUser2"};
|
ScopedUser user2{ session, "MyUser2" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto starredRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||||
EXPECT_FALSE(starredRelease);
|
EXPECT_FALSE(starredRelease);
|
||||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
||||||
|
|
||||||
auto releases {Release::find(session, Release::FindParameters {})};
|
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||||
EXPECT_EQ(releases.results.size(), 1);
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto gotRelease {StarredRelease::find(session, release->getId(), user->getId(), Scrobbler::Internal)};
|
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||||
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||||
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
EXPECT_EQ(StarredRelease::getCount(session), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
auto releases {Release::find(session, Release::FindParameters {})};
|
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||||
EXPECT_EQ(releases.results.size(), 1);
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
|
||||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(releases.results.size(), 1);
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
|
||||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(releases.results.size(), 0);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||||
{
|
{
|
||||||
ScopedRelease release {session, "MyRelease"};
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
ScopedStarredRelease starredRelease {session, release.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
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))};
|
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||||
EXPECT_EQ(releases.results.size(), 1);
|
EXPECT_EQ(releases.results.size(), 1);
|
||||||
|
|
||||||
starredRelease.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
starredRelease.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||||
releases = Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(releases.results.size(), 0);
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||||
{
|
{
|
||||||
ScopedRelease release1 {session, "MyRelease1"};
|
ScopedRelease release1{ session, "MyRelease1" };
|
||||||
ScopedRelease release2 {session, "MyRelease2"};
|
ScopedRelease release2{ session, "MyRelease2" };
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
|
||||||
ScopedStarredRelease starredRelease1 {session, release1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredRelease starredRelease1{ session, release1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||||
ScopedStarredRelease starredRelease2 {session, release2.lockAndGet(), user.lockAndGet(), Scrobbler::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))};
|
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||||
EXPECT_EQ(releases.results.size(), 2);
|
EXPECT_EQ(releases.results.size(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
|
|
||||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||||
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
||||||
EXPECT_EQ(releases.results[0], starredRelease1->getRelease()->getId());
|
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||||
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
}
|
||||||
}
|
{
|
||||||
{
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
auto releases {Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(ReleaseSortMethod::StarredDateDesc)) };
|
||||||
.setSortMethod(ReleaseSortMethod::StarredDateDesc))};
|
ASSERT_EQ(releases.results.size(), 2);
|
||||||
ASSERT_EQ(releases.results.size(), 2);
|
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
||||||
EXPECT_EQ(releases.results[0], starredRelease2->getRelease()->getId());
|
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
||||||
EXPECT_EQ(releases.results[1], starredRelease1->getRelease()->getId());
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,102 +26,100 @@ using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
|
|||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredTrack)
|
TEST_F(DatabaseFixture, StarredTrack)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user {session, "MyUser"};
|
||||||
ScopedUser user2 {session, "MyUser2"};
|
ScopedUser user2 {session, "MyUser2"};
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||||
EXPECT_FALSE(starredTrack);
|
EXPECT_FALSE(starredTrack);
|
||||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||||
|
|
||||||
auto tracks {Track::find(session, Track::FindParameters {})};
|
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||||
EXPECT_EQ(tracks.results.size(), 1);
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), Scrobbler::Internal)};
|
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||||
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||||
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
auto tracks {Track::find(session, Track::FindParameters {})};
|
auto tracks {Track::find(session, Track::FindParameters {})};
|
||||||
EXPECT_EQ(tracks.results.size(), 1);
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
|
||||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(tracks.results.size(), 1);
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
|
||||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), Scrobbler::Internal));
|
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(tracks.results.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||||
{
|
{
|
||||||
ScopedTrack track {session, "MyTrack"};
|
ScopedTrack track {session, "MyTrack"};
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user {session, "MyUser"};
|
||||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
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))};
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||||
EXPECT_EQ(tracks.results.size(), 1);
|
EXPECT_EQ(tracks.results.size(), 1);
|
||||||
|
|
||||||
starredTrack.get().modify()->setScrobblingState(ScrobblingState::PendingRemove);
|
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||||
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal));
|
tracks = Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||||
EXPECT_EQ(tracks.results.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||||
{
|
{
|
||||||
ScopedTrack track1 {session, "MyTrack1"};
|
ScopedTrack track1 {session, "MyTrack1"};
|
||||||
ScopedTrack track2 {session, "MyTrack2"};
|
ScopedTrack track2 {session, "MyTrack2"};
|
||||||
ScopedUser user {session, "MyUser"};
|
ScopedUser user {session, "MyUser"};
|
||||||
|
|
||||||
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), Scrobbler::Internal};
|
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||||
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), Scrobbler::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))};
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||||
EXPECT_EQ(tracks.results.size(), 2);
|
EXPECT_EQ(tracks.results.size(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||||
|
|
||||||
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||||
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||||
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||||
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
}
|
||||||
}
|
{
|
||||||
{
|
auto transaction {session.createUniqueTransaction()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||||
|
|
||||||
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), Scrobbler::Internal)
|
auto tracks {Track::find(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||||
.setSortMethod(TrackSortMethod::StarredDateDesc))};
|
ASSERT_EQ(tracks.results.size(), 2);
|
||||||
ASSERT_EQ(tracks.results.size(), 2);
|
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||||
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||||
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<IFeedbackService> createFeedbackService(boost::asio::io_context& ioContext, Db& db)
|
||||||
|
{
|
||||||
|
return std::make_unique<FeedbackService>(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<InternalBackend>(_db));
|
||||||
|
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||||
|
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackService::~FeedbackService()
|
||||||
|
{
|
||||||
|
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
|
||||||
|
{
|
||||||
|
std::optional<Database::FeedbackBackend> 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<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedbackService::unstar(UserId userId, ArtistId artistId)
|
||||||
|
{
|
||||||
|
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FeedbackService::isStarred(UserId userId, ArtistId artistId)
|
||||||
|
{
|
||||||
|
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ArtistId artistId)
|
||||||
|
{
|
||||||
|
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> 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<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedbackService::unstar(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FeedbackService::isStarred(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ReleaseId releaseId)
|
||||||
|
{
|
||||||
|
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector<ClusterId>& 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<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedbackService::unstar(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FeedbackService::isStarred(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, TrackId trackId)
|
||||||
|
{
|
||||||
|
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector<ClusterId>& 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
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#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<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> 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<Database::ClusterId>& 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<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
|
std::optional<Database::FeedbackBackend> getUserFeedbackBackend(Database::UserId userId);
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void star(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
void unstar(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
bool isStarred(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
|
||||||
|
|
||||||
|
Database::Db& _db;
|
||||||
|
std::unordered_map<Database::FeedbackBackend, std::unique_ptr<IFeedbackBackend>> _backends;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // ns Feedback
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Db.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
namespace Feedback
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
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<StarredObjType>(obj, user, *backend);
|
||||||
|
}
|
||||||
|
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||||
|
starredObjId = starredObj->getId();
|
||||||
|
}
|
||||||
|
_backends[*backend]->onStarred(starredObjId);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
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 <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
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 <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
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
|
||||||
+6
-25
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 Emeric Poupon
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
@@ -19,35 +19,17 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "services/database/StarredArtistId.hpp"
|
#include "services/database/StarredArtistId.hpp"
|
||||||
#include "services/database/StarredReleaseId.hpp"
|
#include "services/database/StarredReleaseId.hpp"
|
||||||
#include "services/database/StarredTrackId.hpp"
|
#include "services/database/StarredTrackId.hpp"
|
||||||
#include "services/scrobbling/Listen.hpp"
|
|
||||||
|
|
||||||
namespace Database
|
namespace Feedback
|
||||||
{
|
{
|
||||||
class Session;
|
class IFeedbackBackend
|
||||||
class TrackList;
|
|
||||||
class User;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Scrobbling
|
|
||||||
{
|
|
||||||
class IScrobbler
|
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IScrobbler() = default;
|
virtual ~IFeedbackBackend() = default;
|
||||||
|
|
||||||
// Listens
|
|
||||||
virtual void listenStarted(const Listen& listen) = 0;
|
|
||||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
|
||||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
|
||||||
|
|
||||||
// Feedbacks
|
|
||||||
virtual void onStarred(Database::StarredArtistId) = 0;
|
virtual void onStarred(Database::StarredArtistId) = 0;
|
||||||
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
||||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||||
@@ -56,7 +38,6 @@ namespace Scrobbling
|
|||||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
std::unique_ptr<IFeedbackBackend> createFeedbackBackend(std::string_view backendName);
|
||||||
|
|
||||||
} // ns Scrobbling
|
|
||||||
|
|
||||||
|
} // ns Feedback
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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 <typename StarredObjType>
|
||||||
|
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 <typename StarredObjType>
|
||||||
|
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<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalBackend::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||||
|
{
|
||||||
|
details::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalBackend::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||||
|
{
|
||||||
|
details::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalBackend::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||||
|
{
|
||||||
|
details::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalBackend::onStarred(Database::StarredTrackId starredTrackId)
|
||||||
|
{
|
||||||
|
details::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalBackend::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||||
|
{
|
||||||
|
details::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||||
|
}
|
||||||
|
} // Feedback
|
||||||
+5
-10
@@ -19,26 +19,21 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IScrobbler.hpp"
|
#include "IFeedbackBackend.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Feedback
|
||||||
{
|
{
|
||||||
class InternalScrobbler final : public IScrobbler
|
class InternalBackend final : public IFeedbackBackend
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InternalScrobbler(Database::Db& db);
|
InternalBackend(Database::Db& db);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// IScrobbler
|
|
||||||
void listenStarted(const Listen& listen) override;
|
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
|
||||||
|
|
||||||
void onStarred(Database::StarredArtistId) override;
|
void onStarred(Database::StarredArtistId) override;
|
||||||
void onUnstarred(Database::StarredArtistId) override;
|
void onUnstarred(Database::StarredArtistId) override;
|
||||||
void onStarred(Database::StarredReleaseId) override;
|
void onStarred(Database::StarredReleaseId) override;
|
||||||
@@ -48,5 +43,5 @@ namespace Scrobbling
|
|||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
};
|
};
|
||||||
} // Scrobbling
|
} // Feedback
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/feedback/Exception.hpp"
|
||||||
|
|
||||||
|
namespace Feedback::ListenBrainz
|
||||||
|
{
|
||||||
|
class Exception : public ::Feedback::Exception
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ::Feedback::Exception::Exception;
|
||||||
|
};
|
||||||
|
}
|
||||||
+2
-2
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include "FeedbackTypes.hpp"
|
#include "FeedbackTypes.hpp"
|
||||||
|
|
||||||
namespace Scrobbling::ListenBrainz
|
namespace Feedback::ListenBrainz
|
||||||
{
|
{
|
||||||
std::ostream&
|
std::ostream&
|
||||||
operator<<(std::ostream& os, const Feedback& feedback)
|
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<int>(feedback.score);
|
os << "created = '" << feedback.created.toString() << "', recording MBID = '" << feedback.recordingMBID.getAsString() << "', score = " << static_cast<int>(feedback.score);
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
} // Scrobbling::ListenBrainz
|
} // Feedback::ListenBrainz
|
||||||
+2
-2
@@ -23,7 +23,7 @@
|
|||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
#include "utils/UUID.hpp"
|
#include "utils/UUID.hpp"
|
||||||
|
|
||||||
namespace Scrobbling::ListenBrainz
|
namespace Feedback::ListenBrainz
|
||||||
{
|
{
|
||||||
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
|
// See https://listenbrainz.readthedocs.io/en/production/dev/feedback-json/#feedback-json-doc
|
||||||
enum class FeedbackType
|
enum class FeedbackType
|
||||||
@@ -42,4 +42,4 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const Feedback& feedback);
|
std::ostream& operator<<(std::ostream& os, const Feedback& feedback);
|
||||||
|
|
||||||
} // Scrobbling::ListenBrainz
|
} // Feedback::ListenBrainz
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FeedbacksParser.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Json/Array.h>
|
||||||
|
#include <Wt/Json/Object.h>
|
||||||
|
#include <Wt/Json/Value.h>
|
||||||
|
#include <Wt/Json/Parser.h>
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace Feedback::ListenBrainz
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Feedback parseFeedback(const Wt::Json::Object& feedbackObj)
|
||||||
|
{
|
||||||
|
const std::optional<UUID> recordingMBID{ UUID::fromString(static_cast<std::string>(feedbackObj.get("recording_mbid"))) };
|
||||||
|
if (!recordingMBID)
|
||||||
|
throw Exception{ "MBID not found!" };
|
||||||
|
|
||||||
|
return Feedback
|
||||||
|
{
|
||||||
|
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
|
||||||
|
*recordingMBID,
|
||||||
|
static_cast<FeedbackType>(static_cast<int>(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
|
||||||
+2
-2
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
#include "FeedbackTypes.hpp"
|
#include "FeedbackTypes.hpp"
|
||||||
|
|
||||||
namespace Scrobbling::ListenBrainz
|
namespace Feedback::ListenBrainz
|
||||||
{
|
{
|
||||||
class FeedbacksParser
|
class FeedbacksParser
|
||||||
{
|
{
|
||||||
@@ -37,4 +37,4 @@ namespace Scrobbling::ListenBrainz
|
|||||||
static Result parse(std::string_view msgBody);
|
static Result parse(std::string_view msgBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // Scrobbling::ListenBrainz
|
} // Feedback::ListenBrainz
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FeedbacksSynchronizer.hpp"
|
||||||
|
|
||||||
|
#include <boost/asio/bind_executor.hpp>
|
||||||
|
#include <Wt/Json/Array.h>
|
||||||
|
#include <Wt/Json/Object.h>
|
||||||
|
#include <Wt/Json/Value.h>
|
||||||
|
#include <Wt/Json/Serializer.h>
|
||||||
|
|
||||||
|
#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<std::size_t> parseTotalFeedbackCount(std::string_view msgBody)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Wt::Json::Object root;
|
||||||
|
Wt::Json::parse(std::string{ msgBody }, root);
|
||||||
|
|
||||||
|
return static_cast<int>(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<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
|
||||||
|
, _syncFeedbacksPeriod{ Service<IConfig>::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<UUID> 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<UUID> 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<int>(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<StarredTrackId> 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<Database::UserId> 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<UUID> 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<Track::pointer> 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<StarredTrack>(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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
#include <boost/asio/io_context_strand.hpp>
|
||||||
|
#include <boost/asio/steady_timer.hpp>
|
||||||
|
|
||||||
|
#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<std::size_t> 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<Database::UserId, UserContext> _userContexts;
|
||||||
|
|
||||||
|
const std::size_t _maxSyncFeedbackCount;
|
||||||
|
const std::chrono::hours _syncFeedbacksPeriod;
|
||||||
|
};
|
||||||
|
} // Feedback::ListenBrainz
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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 <typename StarredObjType>
|
||||||
|
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 <typename StarredObjType>
|
||||||
|
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<IConfig>::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<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListenBrainzBackend::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||||
|
{
|
||||||
|
details::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListenBrainzBackend::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||||
|
{
|
||||||
|
details::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListenBrainzBackend::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||||
|
{
|
||||||
|
details::onUnstarred<Database::StarredRelease>(_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
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#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<Http::IClient> _client;
|
||||||
|
FeedbacksSynchronizer _feedbacksSynchronizer;
|
||||||
|
};
|
||||||
|
} // Feedback::ListenBrainz
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Json/Object.h>
|
||||||
|
#include <Wt/Json/Parser.h>
|
||||||
|
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
namespace Feedback::ListenBrainz::Utils
|
||||||
|
{
|
||||||
|
std::optional<UUID> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||||
|
std::string parseValidateToken(std::string_view msgBody);
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "utils/Exception.hpp"
|
||||||
|
|
||||||
|
namespace Feedback
|
||||||
|
{
|
||||||
|
class Exception : public LmsException
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LmsException::LmsException;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <boost/asio/io_service.hpp>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#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<Database::ArtistId>;
|
||||||
|
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||||
|
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||||
|
|
||||||
|
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<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> 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<Database::ClusterId>& 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<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, Database::Db& db);
|
||||||
|
|
||||||
|
} // ns Feedback
|
||||||
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
|
|
||||||
add_library(lmsscrobbling SHARED
|
add_library(lmsscrobbling SHARED
|
||||||
impl/internal/InternalScrobbler.cpp
|
impl/internal/InternalBackend.cpp
|
||||||
impl/listenbrainz/FeedbacksParser.cpp
|
impl/listenbrainz/ListenBrainzBackend.cpp
|
||||||
impl/listenbrainz/FeedbacksSynchronizer.cpp
|
|
||||||
impl/listenbrainz/FeedbackTypes.cpp
|
|
||||||
impl/listenbrainz/ListenBrainzScrobbler.cpp
|
|
||||||
impl/listenbrainz/ListenTypes.cpp
|
impl/listenbrainz/ListenTypes.cpp
|
||||||
impl/listenbrainz/ListensParser.cpp
|
impl/listenbrainz/ListensParser.cpp
|
||||||
impl/listenbrainz/ListensSynchronizer.cpp
|
impl/listenbrainz/ListensSynchronizer.cpp
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#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<std::chrono::seconds> duration) = 0;
|
||||||
|
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||||
|
};
|
||||||
|
} // ns Scrobbling
|
||||||
|
|
||||||
@@ -18,22 +18,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ScrobblingService.hpp"
|
#include "ScrobblingService.hpp"
|
||||||
#include "ScrobblingService.impl.hpp"
|
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
#include "services/database/Listen.hpp"
|
#include "services/database/Listen.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/Session.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/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
#include "internal/InternalScrobbler.hpp"
|
#include "internal/InternalBackend.hpp"
|
||||||
#include "listenbrainz/ListenBrainzScrobbler.hpp"
|
#include "listenbrainz/ListenBrainzBackend.hpp"
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
@@ -48,8 +44,8 @@ namespace Scrobbling
|
|||||||
: _db{ db }
|
: _db{ db }
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||||
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
_scrobblingBackends.emplace(ScrobblingBackend::Internal, std::make_unique<InternalBackend>(_db));
|
||||||
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
_scrobblingBackends.emplace(ScrobblingBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,46 +56,46 @@ namespace Scrobbling
|
|||||||
|
|
||||||
void ScrobblingService::listenStarted(const Listen& listen)
|
void ScrobblingService::listenStarted(const Listen& listen)
|
||||||
{
|
{
|
||||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
_scrobblingBackends[*backend]->listenStarted(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||||
{
|
{
|
||||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
_scrobblingBackends[*backend]->listenFinished(listen, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrobblingService::addTimedListen(const TimedListen& listen)
|
void ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||||
{
|
{
|
||||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
_scrobblingBackends[*backend]->addTimedListen(listen);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId)
|
std::optional<ScrobblingBackend> ScrobblingService::getUserBackend(UserId userId)
|
||||||
{
|
{
|
||||||
std::optional<Scrobbler> scrobbler;
|
std::optional<ScrobblingBackend> backend;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
if (const User::pointer user{ User::find(session, userId) })
|
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<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
{
|
{
|
||||||
ArtistContainer res;
|
ArtistContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +103,14 @@ namespace Scrobbling
|
|||||||
{
|
{
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getRecentReleases(session, userId, *backend, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,40 +118,40 @@ namespace Scrobbling
|
|||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getRecentTracks(session, userId, *backend, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
|
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
|
||||||
{
|
{
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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{};
|
return listen ? listen->getDateTime() : Wt::WDateTime{};
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
|
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
|
||||||
{
|
{
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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{};
|
return listen ? listen->getDateTime() : Wt::WDateTime{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,14 +160,14 @@ namespace Scrobbling
|
|||||||
{
|
{
|
||||||
ArtistContainer res;
|
ArtistContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,14 +175,14 @@ namespace Scrobbling
|
|||||||
{
|
{
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getTopReleases(session, userId, *backend, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,133 +190,15 @@ namespace Scrobbling
|
|||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
auto scrobbler{ getUserScrobbler(userId) };
|
const auto backend{ getUserBackend(userId) };
|
||||||
if (!scrobbler)
|
if (!backend)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createSharedTransaction() };
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrobblingService::star(UserId userId, ArtistId artistId)
|
|
||||||
{
|
|
||||||
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
|
||||||
{
|
|
||||||
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
|
||||||
{
|
|
||||||
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
|
|
||||||
{
|
|
||||||
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
|
||||||
std::optional<TrackArtistLinkType> 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<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
|
|
||||||
{
|
|
||||||
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& 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<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingService::unstar(UserId userId, TrackId trackId)
|
|
||||||
{
|
|
||||||
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
|
||||||
{
|
|
||||||
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
|
|
||||||
{
|
|
||||||
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& 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
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "IScrobbler.hpp"
|
#include "IScrobblingBackend.hpp"
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
@@ -39,70 +39,21 @@ namespace Scrobbling
|
|||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
|
|
||||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType,Database::Range range) override;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds,Database::Range range) override;
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
TrackContainer getRecentTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
Database::Range range) override;
|
|
||||||
|
|
||||||
ReleaseContainer getRecentReleases(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) override;
|
|
||||||
|
|
||||||
TrackContainer getRecentTracks(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) override;
|
|
||||||
|
|
||||||
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override;
|
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override;
|
||||||
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override;
|
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
|
|
||||||
ArtistContainer getTopArtists(Database::UserId userId,
|
ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) override;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
Database::Range range) override;
|
|
||||||
|
|
||||||
ReleaseContainer getTopReleases(Database::UserId userId,
|
std::optional<Database::ScrobblingBackend> getUserBackend(Database::UserId userId);
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) override;
|
|
||||||
|
|
||||||
TrackContainer getTopTracks(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& 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<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::TrackArtistLinkType> 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<Database::ClusterId>& 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<Database::ClusterId>& clusterIds, Database::Range range) override;
|
|
||||||
|
|
||||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
void star(Database::UserId userId, ObjIdType id);
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
void unstar(Database::UserId userId, ObjIdType id);
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
bool isStarred(Database::UserId userId, ObjIdType id);
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
|
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
std::unordered_map<Database::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "services/database/Db.hpp"
|
|
||||||
#include "services/database/Session.hpp"
|
|
||||||
#include "services/database/User.hpp"
|
|
||||||
|
|
||||||
namespace Scrobbling
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
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<StarredObjType>(obj, user, *scrobbler);
|
|
||||||
}
|
|
||||||
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
|
||||||
starredObjId = starredObj->getId();
|
|
||||||
}
|
|
||||||
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
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 <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
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 <typename ObjType, typename ObjIdType, typename StarredObjType>
|
|
||||||
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
|
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<std::chrono::seconds> 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<Database::Listen>(user, track, Database::ScrobblingBackend::Internal, listen.listenedAt) };
|
||||||
|
dbListen.modify()->setSyncState(Database::SyncState::Synchronized);
|
||||||
|
}
|
||||||
|
} // Scrobbling
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<std::chrono::seconds> duration) override;
|
||||||
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
|
|
||||||
|
Database::Db& _db;
|
||||||
|
};
|
||||||
|
} // Scrobbling
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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 <typename StarredObjType>
|
|
||||||
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 <typename StarredObjType>
|
|
||||||
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<std::chrono::seconds> 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<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt) };
|
|
||||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
|
||||||
{
|
|
||||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
|
||||||
{
|
|
||||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
|
||||||
{
|
|
||||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
|
||||||
{
|
|
||||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
|
||||||
{
|
|
||||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
|
||||||
{
|
|
||||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
|
||||||
}
|
|
||||||
} // Scrobbling
|
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FeedbacksParser.hpp"
|
|
||||||
|
|
||||||
#include <Wt/Json/Array.h>
|
|
||||||
#include <Wt/Json/Object.h>
|
|
||||||
#include <Wt/Json/Value.h>
|
|
||||||
#include <Wt/Json/Parser.h>
|
|
||||||
|
|
||||||
#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<UUID> recordingMBID {UUID::fromString(static_cast<std::string>(feedbackObj.get("recording_mbid")))};
|
|
||||||
if (!recordingMBID)
|
|
||||||
throw Exception {"MBID not found!"};
|
|
||||||
|
|
||||||
return Feedback
|
|
||||||
{
|
|
||||||
Wt::WDateTime::fromTime_t(static_cast<int>(feedbackObj.get("created"))),
|
|
||||||
*recordingMBID,
|
|
||||||
static_cast<FeedbackType>(static_cast<int>(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
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ListenBrainzScrobbler.hpp"
|
|
||||||
|
|
||||||
#include <tuple>
|
|
||||||
#include <boost/asio/bind_executor.hpp>
|
|
||||||
#include <Wt/Json/Array.h>
|
|
||||||
#include <Wt/Json/Object.h>
|
|
||||||
#include <Wt/Json/Value.h>
|
|
||||||
#include <Wt/Json/Serializer.h>
|
|
||||||
|
|
||||||
#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<std::size_t>
|
|
||||||
parseTotalFeedbackCount(std::string_view msgBody)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Wt::Json::Object root;
|
|
||||||
Wt::Json::parse(std::string {msgBody}, root);
|
|
||||||
|
|
||||||
return static_cast<int>(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<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000)}
|
|
||||||
, _syncFeedbacksPeriod {Service<IConfig>::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<UUID> 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<UUID> 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<int>(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<StarredTrackId> 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<Database::UserId> 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<UUID> 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<Track::pointer> 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<StarredTrack>(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
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <boost/asio/io_context.hpp>
|
|
||||||
#include <boost/asio/io_context_strand.hpp>
|
|
||||||
#include <boost/asio/steady_timer.hpp>
|
|
||||||
|
|
||||||
#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<std::size_t> 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<Database::UserId, UserContext> _userContexts;
|
|
||||||
|
|
||||||
const std::size_t _maxSyncFeedbackCount;
|
|
||||||
const std::chrono::hours _syncFeedbacksPeriod;
|
|
||||||
};
|
|
||||||
} // Scrobbling::ListenBrainz
|
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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<std::chrono::seconds>(track->getDuration()).count() << "s";
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Db& db)
|
||||||
|
: _ioContext{ ioContext }
|
||||||
|
, _db{ db }
|
||||||
|
, _baseAPIUrl{ Service<IConfig>::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<std::chrono::seconds> 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
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <boost/asio/io_context.hpp>
|
||||||
|
|
||||||
|
#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<std::chrono::seconds> 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<Http::IClient> _client;
|
||||||
|
ListensSynchronizer _listensSynchronizer;
|
||||||
|
};
|
||||||
|
} // Scrobbling::ListenBrainz
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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<std::chrono::seconds>(track->getDuration()).count() << "s";
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename StarredObjType>
|
|
||||||
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 <typename StarredObjType>
|
|
||||||
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<IConfig>::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<std::chrono::seconds> 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<StarredArtist>(_db.getTLSSession(), starredArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scrobbler::onUnstarred(StarredArtistId starredArtistId)
|
|
||||||
{
|
|
||||||
::onUnstarred<StarredArtist>(_db.getTLSSession(), starredArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scrobbler::onStarred(StarredReleaseId starredReleaseId)
|
|
||||||
{
|
|
||||||
::onStarred<StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scrobbler::onUnstarred(StarredReleaseId starredReleaseId)
|
|
||||||
{
|
|
||||||
::onUnstarred<StarredRelease>(_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
|
|
||||||
|
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <optional>
|
|
||||||
#include <boost/asio/io_context.hpp>
|
|
||||||
|
|
||||||
#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<std::chrono::seconds> 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<Http::IClient> _client;
|
|
||||||
ListensSynchronizer _listensSynchronizer;
|
|
||||||
FeedbacksSynchronizer _feedbacksSynchronizer;
|
|
||||||
};
|
|
||||||
} // Scrobbling::ListenBrainz
|
|
||||||
|
|
||||||
@@ -17,9 +17,8 @@
|
|||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ListenBrainzScrobbler.hpp"
|
#include "ListensSynchronizer.hpp"
|
||||||
|
|
||||||
#include <tuple>
|
|
||||||
#include <boost/asio/bind_executor.hpp>
|
#include <boost/asio/bind_executor.hpp>
|
||||||
#include <Wt/Json/Array.h>
|
#include <Wt/Json/Array.h>
|
||||||
#include <Wt/Json/Object.h>
|
#include <Wt/Json/Object.h>
|
||||||
@@ -248,14 +247,14 @@ namespace Scrobbling::ListenBrainz
|
|||||||
{
|
{
|
||||||
const TimedListen timedListen {listen, timePoint};
|
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
|
// 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.priority = Http::ClientRequestParameters::Priority::Normal;
|
||||||
request.onSuccessFunc = [=](std::string_view)
|
request.onSuccessFunc = [=](std::string_view)
|
||||||
{
|
{
|
||||||
_strand.dispatch([=]
|
_strand.dispatch([=]
|
||||||
{
|
{
|
||||||
if (saveListen(timedListen, Database::ScrobblingState::Synchronized))
|
if (saveListen(timedListen, Database::SyncState::Synchronized))
|
||||||
{
|
{
|
||||||
UserContext& context {getUserContext(listen.userId)};
|
UserContext& context {getUserContext(listen.userId)};
|
||||||
if (context.listenCount)
|
if (context.listenCount)
|
||||||
@@ -293,14 +292,14 @@ namespace Scrobbling::ListenBrainz
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
ListensSynchronizer::saveListen(const TimedListen& listen, Database::ScrobblingState scrobblingState)
|
ListensSynchronizer::saveListen(const TimedListen& listen, Database::SyncState scrobblingState)
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createUniqueTransaction()}; // TODO: unique only if needed
|
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)
|
if (!dbListen)
|
||||||
{
|
{
|
||||||
const User::pointer user {User::find(session, listen.userId)};
|
const User::pointer user {User::find(session, listen.userId)};
|
||||||
@@ -311,18 +310,18 @@ namespace Scrobbling::ListenBrainz
|
|||||||
if (!track)
|
if (!track)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
dbListen = session.create<Database::Listen>(user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt);
|
dbListen = session.create<Database::Listen>(user, track, Database::ScrobblingBackend::ListenBrainz, listen.listenedAt);
|
||||||
dbListen.modify()->setScrobblingState(scrobblingState);
|
dbListen.modify()->setSyncState(scrobblingState);
|
||||||
|
|
||||||
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
|
LOG(DEBUG) << "LISTEN CREATED for user " << user->getLoginName() << ", track '" << track->getName() << "' AT " << listen.listenedAt.toString();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbListen->getScrobblingState() == scrobblingState)
|
if (dbListen->getSyncState() == scrobblingState)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
dbListen.modify()->setScrobblingState(scrobblingState);
|
dbListen.modify()->setSyncState(scrobblingState);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,8 +336,8 @@ namespace Scrobbling::ListenBrainz
|
|||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
Database::Listen::FindParameters params;
|
Database::Listen::FindParameters params;
|
||||||
params.setScrobbler(Database::Scrobbler::ListenBrainz)
|
params.setScrobblingBackend(Database::ScrobblingBackend::ListenBrainz)
|
||||||
.setScrobblingState(Database::ScrobblingState::PendingAdd)
|
.setSyncState(Database::SyncState::PendingAdd)
|
||||||
.setRange(Database::Range {0, 100}); // don't flood too much?
|
.setRange(Database::Range {0, 100}); // don't flood too much?
|
||||||
|
|
||||||
const Database::RangeResults results {Database::Listen::find(session, params)};
|
const Database::RangeResults results {Database::Listen::find(session, params)};
|
||||||
@@ -423,7 +422,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
{
|
{
|
||||||
Database::Session& session {_db.getTLSSession()};
|
Database::Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
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)
|
for (const Database::UserId userId : userIds.results)
|
||||||
@@ -583,7 +582,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
context.matchedListenCount++;
|
context.matchedListenCount++;
|
||||||
|
|
||||||
const Scrobbling::TimedListen listen {{context.userId, trackId}, parsedListen.listenedAt};
|
const Scrobbling::TimedListen listen {{context.userId, trackId}, parsedListen.listenedAt};
|
||||||
if (saveListen(listen, Database::ScrobblingState::Synchronized))
|
if (saveListen(listen, Database::SyncState::Synchronized))
|
||||||
context.importedListenCount++;
|
context.importedListenCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace Scrobbling::ListenBrainz
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
|
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();
|
void enquePendingListens();
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "utils/UUID.hpp"
|
|
||||||
|
|
||||||
#include "services/database/UserId.hpp"
|
#include "services/database/UserId.hpp"
|
||||||
#include "utils/Logger.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
|
namespace Database
|
||||||
{
|
{
|
||||||
@@ -34,6 +33,5 @@ namespace Database
|
|||||||
namespace Scrobbling::ListenBrainz::Utils
|
namespace Scrobbling::ListenBrainz::Utils
|
||||||
{
|
{
|
||||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||||
|
|
||||||
std::string parseValidateToken(std::string_view msgBody);
|
std::string parseValidateToken(std::string_view msgBody);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,59 +55,19 @@ namespace Scrobbling
|
|||||||
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
||||||
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||||
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||||
|
|
||||||
// From most recent to oldest
|
// From most recent to oldest
|
||||||
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
virtual ArtistContainer getRecentArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) = 0;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
virtual ReleaseContainer getRecentReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
virtual TrackContainer getRecentTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
Database::Range range) = 0;
|
|
||||||
|
|
||||||
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) = 0;
|
|
||||||
|
|
||||||
virtual TrackContainer getRecentTracks(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) = 0;
|
|
||||||
|
|
||||||
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
||||||
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
|
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range) = 0;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
Database::Range) = 0;
|
|
||||||
|
|
||||||
virtual ReleaseContainer getTopReleases(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
|
||||||
Database::Range range) = 0;
|
|
||||||
|
|
||||||
virtual TrackContainer getTopTracks(Database::UserId userId,
|
|
||||||
const std::vector<Database::ClusterId>& 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<Database::ClusterId>& clusterIds,
|
|
||||||
std::optional<Database::TrackArtistLinkType> 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<Database::ClusterId>& 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<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ target_link_libraries(lmssubsonic PRIVATE
|
|||||||
lmsauth
|
lmsauth
|
||||||
lmsav
|
lmsav
|
||||||
lmsdatabase
|
lmsdatabase
|
||||||
|
lmsfeedback
|
||||||
lmsrecommendation
|
lmsrecommendation
|
||||||
lmsscanner
|
lmsscanner
|
||||||
lmsscrobbling
|
lmsscrobbling
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "responses/Album.hpp"
|
#include "responses/Album.hpp"
|
||||||
#include "responses/Artist.hpp"
|
#include "responses/Artist.hpp"
|
||||||
@@ -49,7 +50,8 @@ namespace API::Subsonic
|
|||||||
const Range range{ offset, size };
|
const Range range{ offset, size };
|
||||||
|
|
||||||
RangeResults<ReleaseId> releases;
|
RangeResults<ReleaseId> releases;
|
||||||
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||||
|
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
|
||||||
|
|
||||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
else if (type == "frequent")
|
else if (type == "frequent")
|
||||||
{
|
{
|
||||||
releases = scrobbling.getTopReleases(context.userId, {}, range);
|
releases = scrobblingService.getTopReleases(context.userId, {}, range);
|
||||||
}
|
}
|
||||||
else if (type == "newest")
|
else if (type == "newest")
|
||||||
{
|
{
|
||||||
@@ -123,11 +125,11 @@ namespace API::Subsonic
|
|||||||
}
|
}
|
||||||
else if (type == "recent")
|
else if (type == "recent")
|
||||||
{
|
{
|
||||||
releases = scrobbling.getRecentReleases(context.userId, {}, range);
|
releases = scrobblingService.getRecentReleases(context.userId, {}, range);
|
||||||
}
|
}
|
||||||
else if (type == "starred")
|
else if (type == "starred")
|
||||||
{
|
{
|
||||||
releases = scrobbling.getStarredReleases(context.userId, {}, range);
|
releases = feedbackService.getStarredReleases(context.userId, {}, range);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw NotImplementedGenericError{};
|
throw NotImplementedGenericError{};
|
||||||
@@ -155,21 +157,21 @@ namespace API::Subsonic
|
|||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
|
Response::Node& starredNode{ response.createNode(id3 ? Response::Node::Key{ "starred2" } : Response::Node::Key{ "starred" }) };
|
||||||
|
|
||||||
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::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) })
|
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||||
starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
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) })
|
if (auto release{ Release::find(context.dbSession, releaseId) })
|
||||||
starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
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) })
|
if (auto track{ Track::find(context.dbSession, trackId) })
|
||||||
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "services/database/ArtistId.hpp"
|
#include "services/database/ArtistId.hpp"
|
||||||
#include "services/database/ReleaseId.hpp"
|
#include "services/database/ReleaseId.hpp"
|
||||||
#include "services/database/TrackId.hpp"
|
#include "services/database/TrackId.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "ParameterParsing.hpp"
|
#include "ParameterParsing.hpp"
|
||||||
@@ -60,13 +61,13 @@ namespace API::Subsonic
|
|||||||
StarParameters params{ getStarParameters(context.parameters) };
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
for (const ArtistId id : params.artistIds)
|
for (const ArtistId id : params.artistIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
for (const ReleaseId id : params.releaseIds)
|
for (const ReleaseId id : params.releaseIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
for (const TrackId id : params.trackIds)
|
for (const TrackId id : params.trackIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
return Response::createOkResponse(context.serverProtocolVersion);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
@@ -76,13 +77,13 @@ namespace API::Subsonic
|
|||||||
StarParameters params{ getStarParameters(context.parameters) };
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
for (const ArtistId id : params.artistIds)
|
for (const ArtistId id : params.artistIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
for (const ReleaseId id : params.releaseIds)
|
for (const ReleaseId id : params.releaseIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
for (const TrackId id : params.trackIds)
|
for (const TrackId id : params.trackIds)
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
Service<Feedback::IFeedbackService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
return Response::createOkResponse(context.serverProtocolVersion);
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -132,7 +133,7 @@ namespace API::Subsonic
|
|||||||
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ Service<Feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
|
||||||
albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// OpenSubsonic specific fields (must always be set)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "services/database/Release.hpp"
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ namespace API::Subsonic
|
|||||||
artistNode.setAttribute("albumCount", releases.results.size());
|
artistNode.setAttribute("albumCount", releases.results.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ Service<Feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid())
|
||||||
artistNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
artistNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// OpenSubsonic specific fields (must always be set)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -148,7 +149,7 @@ namespace API::Subsonic
|
|||||||
trackResponse.setAttribute("type", "music");
|
trackResponse.setAttribute("type", "music");
|
||||||
trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten()));
|
trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten()));
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ Service<Feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid())
|
||||||
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// Report the first GENRE for this track
|
// Report the first GENRE for this track
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const char* getModuleName(Module mod)
|
|||||||
case Module::DB: return "DB";
|
case Module::DB: return "DB";
|
||||||
case Module::DBUPDATER: return "DB UPDATER";
|
case Module::DBUPDATER: return "DB UPDATER";
|
||||||
case Module::FEATURE: return "FEATURE";
|
case Module::FEATURE: return "FEATURE";
|
||||||
|
case Module::FEEDBACK: return "FEEDBACK";
|
||||||
case Module::HTTP: return "HTTP";
|
case Module::HTTP: return "HTTP";
|
||||||
case Module::MAIN: return "MAIN";
|
case Module::MAIN: return "MAIN";
|
||||||
case Module::METADATA: return "METADATA";
|
case Module::METADATA: return "METADATA";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <unordered_map>
|
#include <utility>
|
||||||
|
|
||||||
#include <boost/algorithm/string/split.hpp>
|
#include <boost/algorithm/string/split.hpp>
|
||||||
#include <boost/algorithm/string/join.hpp>
|
#include <boost/algorithm/string/join.hpp>
|
||||||
@@ -33,6 +33,63 @@
|
|||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr std::pair<char, std::string_view> jsEscapeChars[]
|
||||||
|
{
|
||||||
|
{ '\\', "\\\\" },
|
||||||
|
{ '\n', "\\n" },
|
||||||
|
{ '\r', "\\r" },
|
||||||
|
{ '\t', "\\t" },
|
||||||
|
{ '"', "\\\"" },
|
||||||
|
{ '\'', "\\\'" },
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::pair<char, std::string_view> jsonEscapeChars[]
|
||||||
|
{
|
||||||
|
{ '\\', "\\\\" },
|
||||||
|
{ '\n', "\\n" },
|
||||||
|
{ '\r', "\\r" },
|
||||||
|
{ '\t', "\\t" },
|
||||||
|
{ '"', "\\\"" },
|
||||||
|
{ '\'', "\\\'" },
|
||||||
|
};
|
||||||
|
|
||||||
|
template <std::size_t N>
|
||||||
|
std::string escape(std::string_view str, const std::pair<char, std::string_view>(&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 <std::size_t N>
|
||||||
|
void writeEscapedString(std::ostream& os, std::string_view str, const std::pair<char, std::string_view>(&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<std::string>& results)
|
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||||
{
|
{
|
||||||
std::string curStr;
|
std::string curStr;
|
||||||
@@ -221,54 +278,22 @@ namespace StringUtils
|
|||||||
|
|
||||||
std::string jsEscape(std::string_view str)
|
std::string jsEscape(std::string_view str)
|
||||||
{
|
{
|
||||||
static const std::unordered_map<char, std::string_view> escapeMap
|
return escape(str, jsEscapeChars);
|
||||||
{
|
|
||||||
{ '\\', "\\\\" },
|
|
||||||
{ '\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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeJSEscapedString(std::ostream& os, std::string_view str)
|
void writeJSEscapedString(std::ostream& os, std::string_view str)
|
||||||
{
|
{
|
||||||
static constexpr std::pair<char, std::string_view> charsToEscape[]
|
writeEscapedString(os, str, jsEscapeChars);
|
||||||
{
|
}
|
||||||
{'\\', "\\\\" },
|
|
||||||
{ '\n', "\\n" },
|
std::string jsonEscape(std::string_view str)
|
||||||
{ '\r', "\\r" },
|
{
|
||||||
{ '\t', "\\t" },
|
return escape(str, jsonEscapeChars);
|
||||||
{ '"', "\\\"" },
|
}
|
||||||
{ '\'', "\\\'" },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const char c : str)
|
void writeJsonEscapedString(std::ostream& os, std::string_view str)
|
||||||
{
|
{
|
||||||
auto itEntry{ std::find_if(std::cbegin(charsToEscape), std::cend(charsToEscape), [=](const auto& entry) { return entry.first == c;}) };
|
writeEscapedString(os, str, jsonEscapeChars);
|
||||||
if (itEntry != std::cend(charsToEscape))
|
|
||||||
os << itEntry->second;
|
|
||||||
else
|
|
||||||
os << c;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum class Module
|
|||||||
DB,
|
DB,
|
||||||
DBUPDATER,
|
DBUPDATER,
|
||||||
FEATURE,
|
FEATURE,
|
||||||
|
FEEDBACK,
|
||||||
HTTP,
|
HTTP,
|
||||||
MAIN,
|
MAIN,
|
||||||
METADATA,
|
METADATA,
|
||||||
|
|||||||
@@ -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 replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||||
|
|
||||||
[[nodiscard]] std::string jsEscape(std::string_view str);
|
[[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 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);
|
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ target_link_libraries(lms PRIVATE
|
|||||||
lmsav
|
lmsav
|
||||||
lmsauth
|
lmsauth
|
||||||
lmsdatabase
|
lmsdatabase
|
||||||
|
lmsfeedback
|
||||||
lmsrecommendation
|
lmsrecommendation
|
||||||
lmsscanner
|
lmsscanner
|
||||||
lmsscrobbling
|
lmsscrobbling
|
||||||
|
|||||||
+214
-212
@@ -32,6 +32,7 @@
|
|||||||
#include "services/cover/ICoverService.hpp"
|
#include "services/cover/ICoverService.hpp"
|
||||||
#include "services/database/Db.hpp"
|
#include "services/database/Db.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "services/scanner/IScannerService.hpp"
|
#include "services/scanner/IScannerService.hpp"
|
||||||
@@ -50,84 +51,84 @@ static
|
|||||||
std::size_t
|
std::size_t
|
||||||
getThreadCount()
|
getThreadCount()
|
||||||
{
|
{
|
||||||
const unsigned long configHttpServerThreadCount {Service<IConfig>::get()->getULong("http-server-thread-count", 0)};
|
const unsigned long configHttpServerThreadCount{ Service<IConfig>::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)
|
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
||||||
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
std::vector<std::string>
|
std::vector<std::string>
|
||||||
generateWtConfig(std::string execPath)
|
generateWtConfig(std::string execPath)
|
||||||
{
|
{
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
const std::filesystem::path wtConfigPath {Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
|
const std::filesystem::path wtConfigPath{ Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml" };
|
||||||
const std::filesystem::path wtLogFilePath {Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
|
const std::filesystem::path wtLogFilePath{ Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log") };
|
||||||
const std::filesystem::path wtAccessLogFilePath {Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
|
const std::filesystem::path wtAccessLogFilePath{ Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log") };
|
||||||
const std::filesystem::path wtResourcesPath {Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
|
const std::filesystem::path wtResourcesPath{ Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources") };
|
||||||
|
|
||||||
args.push_back(execPath);
|
args.push_back(execPath);
|
||||||
args.push_back("--config=" + wtConfigPath.string());
|
args.push_back("--config=" + wtConfigPath.string());
|
||||||
args.push_back("--docroot=" + std::string {Service<IConfig>::get()->getString("docroot")});
|
args.push_back("--docroot=" + std::string{ Service<IConfig>::get()->getString("docroot") });
|
||||||
args.push_back("--approot=" + std::string {Service<IConfig>::get()->getString("approot")});
|
args.push_back("--approot=" + std::string{ Service<IConfig>::get()->getString("approot") });
|
||||||
args.push_back("--deploy-path=" + std::string {Service<IConfig>::get()->getString("deploy-path", "/")});
|
args.push_back("--deploy-path=" + std::string{ Service<IConfig>::get()->getString("deploy-path", "/") });
|
||||||
if (!wtResourcesPath.empty())
|
if (!wtResourcesPath.empty())
|
||||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||||
|
|
||||||
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
||||||
{
|
{
|
||||||
args.push_back("--https-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
args.push_back("--https-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
args.push_back("--https-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
|
args.push_back("--https-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
args.push_back("--ssl-certificate=" + std::string {Service<IConfig>::get()->getString("tls-cert")});
|
args.push_back("--ssl-certificate=" + std::string{ Service<IConfig>::get()->getString("tls-cert") });
|
||||||
args.push_back("--ssl-private-key=" + std::string {Service<IConfig>::get()->getString("tls-key")});
|
args.push_back("--ssl-private-key=" + std::string{ Service<IConfig>::get()->getString("tls-key") });
|
||||||
args.push_back("--ssl-tmp-dh=" + std::string {Service<IConfig>::get()->getString("tls-dh")});
|
args.push_back("--ssl-tmp-dh=" + std::string{ Service<IConfig>::get()->getString("tls-dh") });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
args.push_back("--http-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
args.push_back("--http-port=" + std::to_string(Service<IConfig>::get()->getULong("listen-port", 5082)));
|
||||||
args.push_back("--http-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
|
args.push_back("--http-address=" + std::string{ Service<IConfig>::get()->getString("listen-addr", "0.0.0.0") });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wtAccessLogFilePath.empty())
|
if (!wtAccessLogFilePath.empty())
|
||||||
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
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
|
// Generate the wt_config.xml file
|
||||||
boost::property_tree::ptree pt;
|
boost::property_tree::ptree pt;
|
||||||
|
|
||||||
pt.put("server.application-settings.<xmlattr>.location", "*");
|
pt.put("server.application-settings.<xmlattr>.location", "*");
|
||||||
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
||||||
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
||||||
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
||||||
|
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree viewport;
|
boost::property_tree::ptree viewport;
|
||||||
viewport.put("<xmlattr>.name", "viewport");
|
viewport.put("<xmlattr>.name", "viewport");
|
||||||
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
||||||
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
boost::property_tree::ptree themeColor;
|
boost::property_tree::ptree themeColor;
|
||||||
themeColor.put("<xmlattr>.name", "theme-color");
|
themeColor.put("<xmlattr>.name", "theme-color");
|
||||||
themeColor.put("<xmlattr>.content", "#303030");
|
themeColor.put("<xmlattr>.content", "#303030");
|
||||||
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::ofstream oss {wtConfigPath.string().c_str(), std::ios::out};
|
std::ofstream oss{ wtConfigPath.string().c_str(), std::ios::out };
|
||||||
if (!oss)
|
if (!oss)
|
||||||
throw LmsException {"Can't open '" + wtConfigPath.string() + "' for writing!"};
|
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)
|
if (!oss)
|
||||||
throw LmsException {"Can't write in file '" + wtConfigPath.string() + "', no space left?"};
|
throw LmsException{ "Can't write in file '" + wtConfigPath.string() + "', no space left?" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -135,194 +136,195 @@ static
|
|||||||
void
|
void
|
||||||
proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
|
proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
|
||||||
{
|
{
|
||||||
auto postAll {[](Wt::WServer& server, std::function<void()> cb)
|
auto postAll{ [](Wt::WServer& server, std::function<void()> cb)
|
||||||
{
|
{
|
||||||
server.postAll([cb = std::move(cb)]
|
server.postAll([cb = std::move(cb)]
|
||||||
{
|
{
|
||||||
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
||||||
if (LmsApp)
|
if (LmsApp)
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}};
|
} };
|
||||||
|
|
||||||
scanner.getEvents().scanStarted.connect([&]
|
scanner.getEvents().scanStarted.connect([&]
|
||||||
{
|
{
|
||||||
postAll(server, []
|
postAll(server, []
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanStarted.emit();
|
LmsApp->getScannerEvents().scanStarted.emit();
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scanner.getEvents().scanComplete.connect([&] (const Scanner::ScanStats& stats)
|
scanner.getEvents().scanComplete.connect([&](const Scanner::ScanStats& stats)
|
||||||
{
|
{
|
||||||
postAll(server, [=]
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scanner.getEvents().scanInProgress.connect([&] (const Scanner::ScanStepStats& stats)
|
scanner.getEvents().scanInProgress.connect([&](const Scanner::ScanStepStats& stats)
|
||||||
{
|
{
|
||||||
postAll(server, [=]
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
scanner.getEvents().scanScheduled.connect([&] (const Wt::WDateTime dateTime)
|
scanner.getEvents().scanScheduled.connect([&](const Wt::WDateTime dateTime)
|
||||||
{
|
{
|
||||||
postAll(server, [=]
|
postAll(server, [=]
|
||||||
{
|
{
|
||||||
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
||||||
LmsApp->triggerUpdate();
|
LmsApp->triggerUpdate();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
std::filesystem::path configFilePath {"/etc/lms.conf"};
|
std::filesystem::path configFilePath{ "/etc/lms.conf" };
|
||||||
int res {EXIT_FAILURE};
|
int res{ EXIT_FAILURE };
|
||||||
|
|
||||||
assert(argc > 0);
|
assert(argc > 0);
|
||||||
assert(argv[0] != NULL);
|
assert(argv[0] != NULL);
|
||||||
|
|
||||||
if (argc == 2)
|
if (argc == 2)
|
||||||
configFilePath = std::string(argv[1], 0, 256);
|
configFilePath = std::string(argv[1], 0, 256);
|
||||||
else if (argc > 2)
|
else if (argc > 2)
|
||||||
{
|
{
|
||||||
std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n"
|
std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n"
|
||||||
<< "Options:\n"
|
<< "Options:\n"
|
||||||
<< "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n";
|
<< "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
close(STDIN_FILENO);
|
close(STDIN_FILENO);
|
||||||
|
|
||||||
Service<IConfig> config {createConfig(configFilePath)};
|
Service<IConfig> config{ createConfig(configFilePath) };
|
||||||
Service<Logger> logger {std::make_unique<WtLogger>()};
|
Service<Logger> logger{ std::make_unique<WtLogger>() };
|
||||||
|
|
||||||
// use system locale. libarchive relies on this to write filenames
|
// use system locale. libarchive relies on this to write filenames
|
||||||
if (char *locale {::setlocale(LC_ALL, "")})
|
if (char* locale{ ::setlocale(LC_ALL, "") })
|
||||||
LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'";
|
LMS_LOG(MAIN, INFO) << "locale set to '" << locale << "'";
|
||||||
else
|
else
|
||||||
LMS_LOG(MAIN, WARNING) << "Cannot set locale from system";
|
LMS_LOG(MAIN, WARNING) << "Cannot set locale from system";
|
||||||
|
|
||||||
// Make sure the working directory exists
|
// Make sure the working directory exists
|
||||||
std::filesystem::create_directories(config->getPath("working-dir"));
|
std::filesystem::create_directories(config->getPath("working-dir"));
|
||||||
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
||||||
|
|
||||||
// Construct WT configuration and get the argc/argv back
|
// Construct WT configuration and get the argc/argv back
|
||||||
const std::vector<std::string> wtServerArgs {generateWtConfig(argv[0])};
|
const std::vector<std::string> wtServerArgs{ generateWtConfig(argv[0]) };
|
||||||
|
|
||||||
std::vector<const char*> wtArgv(wtServerArgs.size());
|
std::vector<const char*> wtArgv(wtServerArgs.size());
|
||||||
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
||||||
{
|
{
|
||||||
std::cout << "ARG = " << wtServerArgs[i] << std::endl;
|
std::cout << "ARG = " << wtServerArgs[i] << std::endl;
|
||||||
wtArgv[i] = wtServerArgs[i].c_str();
|
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
|
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]};
|
Wt::WServer server{ argv[0] };
|
||||||
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
|
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
|
||||||
|
|
||||||
IOContextRunner ioContextRunner {ioContext, getThreadCount()};
|
IOContextRunner ioContextRunner{ ioContext, getThreadCount() };
|
||||||
|
|
||||||
// Initializing a connection pool to the database that will be shared along services
|
// Initializing a connection pool to the database that will be shared along services
|
||||||
Database::Db database {config->getPath("working-dir") / "lms.db", getThreadCount()};
|
Database::Db database{ config->getPath("working-dir") / "lms.db", getThreadCount() };
|
||||||
{
|
{
|
||||||
Database::Session session {database};
|
Database::Session session{ database };
|
||||||
session.prepareTables();
|
session.prepareTables();
|
||||||
|
|
||||||
// force optimize in case scanner aborted during a large import:
|
// 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
|
// queries may be too slow to even be able to relaunch a scan sing the web interface
|
||||||
session.analyze();
|
session.analyze();
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInterface::LmsApplicationManager appManager;
|
UserInterface::LmsApplicationManager appManager;
|
||||||
|
|
||||||
// Service initialization order is important (reverse-order for deinit)
|
// Service initialization order is important (reverse-order for deinit)
|
||||||
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager(ioContext)};
|
Service<IChildProcessManager> childProcessManagerService{ createChildProcessManager(ioContext) };
|
||||||
Service<Auth::IAuthTokenService> authTokenService;
|
Service<Auth::IAuthTokenService> authTokenService;
|
||||||
Service<Auth::IPasswordService> authPasswordService;
|
Service<Auth::IPasswordService> authPasswordService;
|
||||||
Service<Auth::IEnvService> authEnvService;
|
Service<Auth::IEnvService> authEnvService;
|
||||||
|
|
||||||
const std::string authenticationBackend {StringUtils::stringToLower(config->getString("authentication-backend", "internal"))};
|
const std::string authenticationBackend{ StringUtils::stringToLower(config->getString("authentication-backend", "internal")) };
|
||||||
if (authenticationBackend == "internal" || authenticationBackend == "pam")
|
if (authenticationBackend == "internal" || authenticationBackend == "pam")
|
||||||
{
|
{
|
||||||
authTokenService.assign(Auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000)));
|
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()));
|
authPasswordService.assign(Auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get()));
|
||||||
}
|
}
|
||||||
else if (authenticationBackend == "http-headers")
|
else if (authenticationBackend == "http-headers")
|
||||||
{
|
{
|
||||||
authEnvService.assign(Auth::createEnvService(authenticationBackend, database));
|
authEnvService.assign(Auth::createEnvService(authenticationBackend, database));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"};
|
throw LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
|
||||||
|
|
||||||
Image::init(argv[0]);
|
Image::init(argv[0]);
|
||||||
Service<Cover::ICoverService> coverService {Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")};
|
Service<Cover::ICoverService> coverService{ Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") };
|
||||||
Service<Recommendation::IRecommendationService> recommendationService {Recommendation::createRecommendationService(database)};
|
Service<Recommendation::IRecommendationService> recommendationService{ Recommendation::createRecommendationService(database) };
|
||||||
Service<Recommendation::IPlaylistGeneratorService> playlistGeneratorService {Recommendation::createPlaylistGeneratorService(database, *recommendationService.get())};
|
Service<Recommendation::IPlaylistGeneratorService> playlistGeneratorService{ Recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
|
||||||
Service<Scanner::IScannerService> scannerService {Scanner::createScannerService(database, *recommendationService)};
|
Service<Scanner::IScannerService> scannerService{ Scanner::createScannerService(database, *recommendationService) };
|
||||||
|
|
||||||
scannerService->getEvents().scanComplete.connect([&]
|
scannerService->getEvents().scanComplete.connect([&]
|
||||||
{
|
{
|
||||||
// Flush cover cache even if no changes:
|
// Flush cover cache even if no changes:
|
||||||
// covers may be external files that changed and we don't keep track of them
|
// covers may be external files that changed and we don't keep track of them
|
||||||
coverService->flushCache();
|
coverService->flushCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
Service<Scrobbling::IScrobblingService> scrobblingService {Scrobbling::createScrobblingService(ioContext, database)};
|
Service<Feedback::IFeedbackService> feedbackService{ Feedback::createFeedbackService(ioContext, database) };
|
||||||
|
Service<Scrobbling::IScrobblingService> scrobblingService{ Scrobbling::createScrobblingService(ioContext, database) };
|
||||||
|
|
||||||
std::unique_ptr<Wt::WResource> subsonicResource;
|
std::unique_ptr<Wt::WResource> subsonicResource;
|
||||||
|
|
||||||
// bind API resources
|
// bind API resources
|
||||||
if (config->getBool("api-subsonic", true))
|
if (config->getBool("api-subsonic", true))
|
||||||
{
|
{
|
||||||
subsonicResource = API::Subsonic::createSubsonicResource(database);
|
subsonicResource = API::Subsonic::createSubsonicResource(database);
|
||||||
server.addResource(subsonicResource.get(), "/rest");
|
server.addResource(subsonicResource.get(), "/rest");
|
||||||
}
|
}
|
||||||
|
|
||||||
// bind UI entry point
|
// bind UI entry point
|
||||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||||
[&](const Wt::WEnvironment &env)
|
[&](const Wt::WEnvironment& env)
|
||||||
{
|
{
|
||||||
return UserInterface::LmsApplication::create(env, database, appManager);
|
return UserInterface::LmsApplication::create(env, database, appManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
proxyScannerEventsToApplication(*scannerService, server);
|
proxyScannerEventsToApplication(*scannerService, server);
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Starting server...";
|
LMS_LOG(MAIN, INFO) << "Starting server...";
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Now running...";
|
LMS_LOG(MAIN, INFO) << "Now running...";
|
||||||
Wt::WServer::waitForShutdown();
|
Wt::WServer::waitForShutdown();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Stopping server...";
|
LMS_LOG(MAIN, INFO) << "Stopping server...";
|
||||||
server.stop();
|
server.stop();
|
||||||
|
|
||||||
LMS_LOG(MAIN, INFO) << "Quitting...";
|
LMS_LOG(MAIN, INFO) << "Quitting...";
|
||||||
res = EXIT_SUCCESS;
|
res = EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
catch (const Wt::WServer::Exception& e)
|
catch (const Wt::WServer::Exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what();
|
LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what();
|
||||||
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
|
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
|
||||||
res = EXIT_FAILURE;
|
res = EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
catch (const std::exception& e)
|
catch (const std::exception& e)
|
||||||
{
|
{
|
||||||
LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what();
|
LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what();
|
||||||
std::cerr << "Caught std::exception: " << e.what() << std::endl;
|
std::cerr << "Caught std::exception: " << e.what() << std::endl;
|
||||||
res = EXIT_FAILURE;
|
res = EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
#include "services/recommendation/IPlaylistGeneratorService.hpp"
|
||||||
#include "utils/IConfig.hpp"
|
#include "utils/IConfig.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
@@ -602,7 +602,7 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
|||||||
loadTrack(*pos, true);
|
loadTrack(*pos, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||||
|
|
||||||
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||||
starBtn->clicked().connect([=]
|
starBtn->clicked().connect([=]
|
||||||
@@ -611,12 +611,12 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
|||||||
|
|
||||||
if (isStarred())
|
if (isStarred())
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+537
-503
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
@@ -30,107 +31,105 @@
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
RangeResults<ArtistId>
|
RangeResults<ArtistId> ArtistCollector::get(Database::Range range)
|
||||||
ArtistCollector::get(Database::Range range)
|
{
|
||||||
{
|
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
|
||||||
Scrobbling::IScrobblingService& scrobbling {*Service<Scrobbling::IScrobblingService>::get()};
|
Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||||
|
|
||||||
range = getActualRange(range);
|
range = getActualRange(range);
|
||||||
|
|
||||||
RangeResults<ArtistId> artists;
|
RangeResults<ArtistId> artists;
|
||||||
switch (getMode())
|
switch (getMode())
|
||||||
{
|
{
|
||||||
case Mode::Random:
|
case Mode::Random:
|
||||||
artists = getRandomArtists(range);
|
artists = getRandomArtists(range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::Starred:
|
case Mode::Starred:
|
||||||
artists = scrobbling.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range);
|
artists = feedbackService.getStarredArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, ArtistSortMethod::StarredDateDesc, range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::RecentlyPlayed:
|
case Mode::RecentlyPlayed:
|
||||||
artists = scrobbling.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range);
|
artists = scrobblingService.getRecentArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::MostPlayed:
|
case Mode::MostPlayed:
|
||||||
artists = scrobbling.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range);
|
artists = scrobblingService.getTopArtists(LmsApp->getUserId(), getFilters().getClusterIds(), _linkType, range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::RecentlyAdded:
|
case Mode::RecentlyAdded:
|
||||||
{
|
{
|
||||||
Artist::FindParameters params;
|
Artist::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setLinkType(_linkType);
|
params.setLinkType(_linkType);
|
||||||
params.setSortMethod(ArtistSortMethod::LastWritten);
|
params.setSortMethod(ArtistSortMethod::LastWritten);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
artists = Artist::find(LmsApp->getDbSession(), params);
|
artists = Artist::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Mode::Search:
|
case Mode::Search:
|
||||||
{
|
{
|
||||||
Artist::FindParameters params;
|
Artist::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setKeywords(getSearchKeywords());
|
params.setKeywords(getSearchKeywords());
|
||||||
params.setLinkType(_linkType);
|
params.setLinkType(_linkType);
|
||||||
params.setSortMethod(ArtistSortMethod::BySortName);
|
params.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
artists = Artist::find(LmsApp->getDbSession(), params);
|
artists = Artist::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Mode::All:
|
case Mode::All:
|
||||||
{
|
{
|
||||||
Artist::FindParameters params;
|
Artist::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setLinkType(_linkType);
|
params.setLinkType(_linkType);
|
||||||
params.setSortMethod(ArtistSortMethod::BySortName);
|
params.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
artists = Artist::find(LmsApp->getDbSession(), params);
|
artists = Artist::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.offset + range.size == getMaxCount())
|
if (range.offset + range.size == getMaxCount())
|
||||||
artists.moreResults = false;
|
artists.moreResults = false;
|
||||||
|
|
||||||
return artists;
|
return artists;
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<Database::ArtistId>
|
RangeResults<Database::ArtistId> ArtistCollector::getRandomArtists(Range range)
|
||||||
ArtistCollector::getRandomArtists(Range range)
|
{
|
||||||
{
|
assert(getMode() == Mode::Random);
|
||||||
assert(getMode() == Mode::Random);
|
|
||||||
|
|
||||||
if (!_randomArtists)
|
if (!_randomArtists)
|
||||||
{
|
{
|
||||||
Artist::FindParameters params;
|
Artist::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setLinkType(_linkType);
|
params.setLinkType(_linkType);
|
||||||
params.setSortMethod(ArtistSortMethod::Random);
|
params.setSortMethod(ArtistSortMethod::Random);
|
||||||
params.setRange(Range {0, getMaxCount()});
|
params.setRange(Range{ 0, getMaxCount() });
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
_randomArtists = Artist::find(LmsApp->getDbSession(), params);
|
_randomArtists = Artist::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _randomArtists->getSubRange(range);
|
return _randomArtists->getSubRange(range);
|
||||||
}
|
}
|
||||||
} // ns UserInterface
|
} // ns UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
@@ -187,19 +187,19 @@ Artist::refreshView()
|
|||||||
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(_artistId)});
|
->setLink(Wt::WLink {std::make_unique<DownloadArtistResource>(_artistId)});
|
||||||
|
|
||||||
{
|
{
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), _artistId); }};
|
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), _artistId); }};
|
||||||
|
|
||||||
Wt::WPushButton* starBtn {bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
Wt::WPushButton* starBtn {bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||||
starBtn->clicked().connect([=]
|
starBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
if (isStarred())
|
if (isStarred())
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), _artistId);
|
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), _artistId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), _artistId);
|
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), _artistId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
@@ -32,104 +33,105 @@
|
|||||||
|
|
||||||
namespace UserInterface
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
RangeResults<ReleaseId>
|
||||||
ReleaseCollector::get(Database::Range range)
|
ReleaseCollector::get(Database::Range range)
|
||||||
{
|
{
|
||||||
Scrobbling::IScrobblingService& scrobbling {*Service<Scrobbling::IScrobblingService>::get()};
|
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
|
||||||
|
Scrobbling::IScrobblingService& scrobblingService{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||||
|
|
||||||
range = getActualRange(range);
|
range = getActualRange(range);
|
||||||
|
|
||||||
RangeResults<ReleaseId> releases;
|
RangeResults<ReleaseId> releases;
|
||||||
switch (getMode())
|
switch (getMode())
|
||||||
{
|
{
|
||||||
case Mode::Random:
|
case Mode::Random:
|
||||||
releases = getRandomReleases(range);
|
releases = getRandomReleases(range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::Starred:
|
case Mode::Starred:
|
||||||
releases = scrobbling.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
releases = feedbackService.getStarredReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ReleaseCollector::Mode::RecentlyPlayed:
|
case ReleaseCollector::Mode::RecentlyPlayed:
|
||||||
releases = scrobbling.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
releases = scrobblingService.getRecentReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::MostPlayed:
|
case Mode::MostPlayed:
|
||||||
releases = scrobbling.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
releases = scrobblingService.getTopReleases(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::RecentlyAdded:
|
case Mode::RecentlyAdded:
|
||||||
{
|
{
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
releases = Release::find(LmsApp->getDbSession(), params);
|
releases = Release::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Mode::Search:
|
case Mode::Search:
|
||||||
{
|
{
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setKeywords(getSearchKeywords());
|
params.setKeywords(getSearchKeywords());
|
||||||
params.setSortMethod(ReleaseSortMethod::Name);
|
params.setSortMethod(ReleaseSortMethod::Name);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
releases = Release::find(LmsApp->getDbSession(), params);
|
releases = Release::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Mode::All:
|
case Mode::All:
|
||||||
{
|
{
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setSortMethod(ReleaseSortMethod::Name);
|
params.setSortMethod(ReleaseSortMethod::Name);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
releases = Release::find(LmsApp->getDbSession(), params);
|
releases = Release::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.offset + range.size == getMaxCount())
|
if (range.offset + range.size == getMaxCount())
|
||||||
releases.moreResults = false;
|
releases.moreResults = false;
|
||||||
|
|
||||||
return releases;
|
return releases;
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
RangeResults<ReleaseId>
|
||||||
ReleaseCollector::getRandomReleases(Range range)
|
ReleaseCollector::getRandomReleases(Range range)
|
||||||
{
|
{
|
||||||
assert(getMode() == Mode::Random);
|
assert(getMode() == Mode::Random);
|
||||||
|
|
||||||
if (!_randomReleases)
|
if (!_randomReleases)
|
||||||
{
|
{
|
||||||
Release::FindParameters params;
|
Release::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setSortMethod(ReleaseSortMethod::Random);
|
params.setSortMethod(ReleaseSortMethod::Random);
|
||||||
params.setRange({0, getMaxCount()});
|
params.setRange({ 0, getMaxCount() });
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
auto transaction{ LmsApp->getDbSession().createSharedTransaction() };
|
||||||
_randomReleases = Release::find(LmsApp->getDbSession(), params);
|
_randomReleases = Release::find(LmsApp->getDbSession(), params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _randomReleases->getSubRange(range);
|
return _randomReleases->getSubRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // ns UserInterface
|
} // ns UserInterface
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
#include "common/Template.hpp"
|
#include "common/Template.hpp"
|
||||||
@@ -314,19 +314,19 @@ Release::refreshView()
|
|||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), _releaseId); }};
|
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), _releaseId); }};
|
||||||
|
|
||||||
Wt::WPushButton* starBtn {bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
Wt::WPushButton* starBtn {bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||||
starBtn->clicked().connect([=]
|
starBtn->clicked().connect([=]
|
||||||
{
|
{
|
||||||
if (isStarred())
|
if (isStarred())
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), _releaseId);
|
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), _releaseId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), _releaseId);
|
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), _releaseId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -435,7 +435,7 @@ Release::refreshView()
|
|||||||
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, {trackId});
|
||||||
});
|
});
|
||||||
|
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||||
|
|
||||||
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||||
starBtn->clicked().connect([=]
|
starBtn->clicked().connect([=]
|
||||||
@@ -444,12 +444,12 @@ Release::refreshView()
|
|||||||
|
|
||||||
if (isStarred())
|
if (isStarred())
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackList.hpp"
|
#include "services/database/TrackList.hpp"
|
||||||
#include "services/database/User.hpp"
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
@@ -37,7 +38,9 @@ namespace UserInterface
|
|||||||
RangeResults<TrackId>
|
RangeResults<TrackId>
|
||||||
TrackCollector::get(Range range)
|
TrackCollector::get(Range range)
|
||||||
{
|
{
|
||||||
Scrobbling::IScrobblingService& scrobbling {*Service<Scrobbling::IScrobblingService>::get()};
|
Feedback::IFeedbackService& feedbackService{*Service<Feedback::IFeedbackService>::get()};
|
||||||
|
Scrobbling::IScrobblingService& scrobblingService {*Service<Scrobbling::IScrobblingService>::get()};
|
||||||
|
|
||||||
range = getActualRange(range);
|
range = getActualRange(range);
|
||||||
|
|
||||||
RangeResults<TrackId> tracks;
|
RangeResults<TrackId> tracks;
|
||||||
@@ -48,15 +51,15 @@ namespace UserInterface
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::Starred:
|
case Mode::Starred:
|
||||||
tracks = scrobbling.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
tracks = feedbackService.getStarredTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TrackCollector::Mode::RecentlyPlayed:
|
case TrackCollector::Mode::RecentlyPlayed:
|
||||||
tracks = scrobbling.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
tracks = scrobblingService.getRecentTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::MostPlayed:
|
case Mode::MostPlayed:
|
||||||
tracks = scrobbling.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
tracks = scrobblingService.getTopTracks(LmsApp->getUserId(), getFilters().getClusterIds(), range);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Mode::RecentlyAdded:
|
case Mode::RecentlyAdded:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/Track.hpp"
|
#include "services/database/Track.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ namespace UserInterface::TrackListHelpers
|
|||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
auto isStarred {[=] { return Service<Feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); }};
|
||||||
|
|
||||||
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
Wt::WPushButton* starBtn {entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star"))};
|
||||||
starBtn->clicked().connect([=]
|
starBtn->clicked().connect([=]
|
||||||
@@ -226,12 +226,12 @@ namespace UserInterface::TrackListHelpers
|
|||||||
|
|
||||||
if (isStarred())
|
if (isStarred())
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->unstar(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.star"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Service<Scrobbling::IScrobblingService>::get()->star(LmsApp->getUserId(), trackId);
|
Service<Feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
|
||||||
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
starBtn->setText(Wt::WString::tr("Lms.Explore.unstar"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user