Listenbrainz: save listen even if send failed

This commit is contained in:
emeric
2022-01-24 21:28:28 +01:00
parent fba20fcb7d
commit 86b16266b6
5 changed files with 196 additions and 142 deletions
@@ -24,15 +24,13 @@
#include "services/database/ArtistId.hpp"
#include "services/database/ClusterId.hpp"
#include "services/database/IdType.hpp"
#include "services/database/ListenId.hpp"
#include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "services/database/Types.hpp"
#include "services/database/UserId.hpp"
LMS_DECLARE_IDTYPE(ListenId)
namespace Database
{
@@ -90,6 +88,11 @@ class Listen : public Object<Listen, ListenId>
const std::vector<ClusterId>& clusterIds,
Range range = {});
ScrobblingState getScrobblingState() const { return _scrobblingState; }
ObjectPtr<User> getUser() const { return _user; }
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
template<class Action>
void persist(Action& a)
{
@@ -0,0 +1,25 @@
/*
* 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/database/IdType.hpp"
LMS_DECLARE_IDTYPE(ListenId)
@@ -19,18 +19,9 @@
#include "ListenBrainzScrobbler.hpp"
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Serializer.h>
#include "services/database/Artist.hpp"
#include "services/database/Db.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "services/database/User.hpp"
#include "utils/IConfig.hpp"
#include "utils/http/IClient.hpp"
#include "utils/Logger.hpp"
@@ -56,86 +47,6 @@ namespace
return res;
}
std::optional<Wt::Json::Object>
listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{
auto transaction {session.createSharedTransaction()};
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
if (!track)
return std::nullopt;
auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
if (artists.empty())
artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist});
if (artists.empty())
{
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
return std::nullopt;
}
Wt::Json::Object additionalInfo;
additionalInfo["listening_from"] = "LMS";
if (track->getRelease())
{
if (auto MBID {track->getRelease()->getMBID()})
additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
}
{
Wt::Json::Array artistMBIDs;
for (const Database::Artist::pointer& artist : artists)
{
if (auto MBID {artist->getMBID()})
artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}});
}
if (!artistMBIDs.empty())
additionalInfo["artist_mbids"] = std::move(artistMBIDs);
}
if (auto MBID {track->getTrackMBID()})
additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
if (auto MBID {track->getRecordingMBID()})
additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
if (const std::optional<std::size_t> trackNumber {track->getTrackNumber()})
additionalInfo["tracknumber"] = Wt::Json::Value {static_cast<long long int>(*trackNumber)};
Wt::Json::Object trackMetadata;
trackMetadata["additional_info"] = std::move(additionalInfo);
trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()};
trackMetadata["track_name"] = Wt::Json::Value {track->getName()};
if (track->getRelease())
trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()};
Wt::Json::Object payload;
payload["track_metadata"] = std::move(trackMetadata);
if (timePoint.isValid())
payload["listened_at"] = Wt::Json::Value {static_cast<long long int>(timePoint.toTime_t())};
return payload;
}
std::string
listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType)
{
std::string res;
std::optional<Wt::Json::Object> payload {listenToJsonPayload(session, listen, timePoint)};
if (!payload)
return res;
Wt::Json::Object root;
root["listen_type"] = Wt::Json::Value {std::string {listenType}};
root["payload"] = Wt::Json::Array {std::move(*payload)};
res = Wt::Json::serialize(root);
return res;
}
}
namespace Scrobbling::ListenBrainz
@@ -183,38 +94,7 @@ namespace Scrobbling::ListenBrainz
void
Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
{
Http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/submit-listens";
if (timePoint.isValid())
{
request.priority = Http::ClientRequestParameters::Priority::Normal;
request.onSuccessFunc = [=](std::string_view)
{
_listensSynchronizer.saveListen(TimedListen {listen, timePoint});
};
}
else
{
// We want "listen now" to appear as soon as possible
request.priority = Http::ClientRequestParameters::Priority::High;
}
std::string bodyText {listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now")};
if (bodyText.empty())
{
LOG(DEBUG) << "Cannot convert listen to json: skipping";
return;
}
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId)};
if (!listenBrainzToken)
return;
request.message.addBodyText(bodyText);
request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
request.message.addHeader("Content-Type", "application/json");
_client->sendPOSTRequest(std::move(request));
_listensSynchronizer.enqueListen(listen, timePoint);
}
} // namespace Scrobbling::ListenBrainz
@@ -47,6 +47,86 @@ namespace
{
using namespace Scrobbling::ListenBrainz;
std::optional<Wt::Json::Object>
listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{
auto transaction {session.createSharedTransaction()};
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
if (!track)
return std::nullopt;
auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
if (artists.empty())
artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist});
if (artists.empty())
{
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
return std::nullopt;
}
Wt::Json::Object additionalInfo;
additionalInfo["listening_from"] = "LMS";
if (track->getRelease())
{
if (auto MBID {track->getRelease()->getMBID()})
additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
}
{
Wt::Json::Array artistMBIDs;
for (const Database::Artist::pointer& artist : artists)
{
if (auto MBID {artist->getMBID()})
artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}});
}
if (!artistMBIDs.empty())
additionalInfo["artist_mbids"] = std::move(artistMBIDs);
}
if (auto MBID {track->getTrackMBID()})
additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
if (auto MBID {track->getRecordingMBID()})
additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
if (const std::optional<std::size_t> trackNumber {track->getTrackNumber()})
additionalInfo["tracknumber"] = Wt::Json::Value {static_cast<long long int>(*trackNumber)};
Wt::Json::Object trackMetadata;
trackMetadata["additional_info"] = std::move(additionalInfo);
trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()};
trackMetadata["track_name"] = Wt::Json::Value {track->getName()};
if (track->getRelease())
trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()};
Wt::Json::Object payload;
payload["track_metadata"] = std::move(trackMetadata);
if (timePoint.isValid())
payload["listened_at"] = Wt::Json::Value {static_cast<long long int>(timePoint.toTime_t())};
return payload;
}
std::string
listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType)
{
std::string res;
std::optional<Wt::Json::Object> payload {listenToJsonPayload(session, listen, timePoint)};
if (!payload)
return res;
Wt::Json::Object root;
root["listen_type"] = Wt::Json::Value {std::string {listenType}};
root["payload"] = Wt::Json::Array {std::move(*payload)};
res = Wt::Json::serialize(root);
return res;
}
std::string
parseValidateToken(std::string_view msgBody)
{
@@ -226,30 +306,91 @@ namespace Scrobbling::ListenBrainz
}
void
ListensSynchronizer::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
{
Http::ClientPOSTRequestParameters request;
request.relativeUrl = "/1/submit-listens";
if (timePoint.isValid())
{
// We want the listen to be sent again later in case of failure, so we just save it as pending send
const Database::ListenId listenId {saveListen(TimedListen {listen, timePoint})};
if (!listenId.isValid())
return;
request.priority = Http::ClientRequestParameters::Priority::Normal;
request.onSuccessFunc = [=](std::string_view)
{
onListenSent(listenId);
};
// on failure, this listen will be sent during the next sync
}
else
{
// We want "listen now" to appear as soon as possible
request.priority = Http::ClientRequestParameters::Priority::High;
// don't retry on failure
}
std::string bodyText {listenToJsonString(_db.getTLSSession(), listen, timePoint, timePoint.isValid() ? "single" : "playing_now")};
if (bodyText.empty())
{
LOG(DEBUG) << "Cannot convert listen to json: skipping";
return;
}
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(_db.getTLSSession(), listen.userId)};
if (!listenBrainzToken)
return;
request.message.addBodyText(bodyText);
request.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
request.message.addHeader("Content-Type", "application/json");
_client.sendPOSTRequest(std::move(request));
}
Database::ListenId
ListensSynchronizer::saveListen(const TimedListen& listen)
{
using namespace Database;
Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt))
return {};
const User::pointer user {User::find(session, listen.userId)};
if (!user)
return {};
const Track::pointer track {Track::find(session, listen.trackId)};
if (!track)
return {};
const auto dbListen {Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt)};
assert(dbListen->getScrobblingState() == Database::ScrobblingState::PendingAdd);
return dbListen->getId();
}
void
ListensSynchronizer::onListenSent(Database::ListenId listenId)
{
_strand.dispatch([=]
{
Database::Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()};
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::ListenBrainz, listen.listenedAt))
return;
if (Database::Listen::pointer listen {Database::Listen::find(session, listenId)})
{
listen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
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;
Database::Listen::create(session, user, track, Database::Scrobbler::ListenBrainz, listen.listenedAt);
UserContext& context {getUserContext(listen.userId)};
if (context.listenCount)
(*context.listenCount)++;
UserContext& context {getUserContext(listen->getUser()->getId())};
if (context.listenCount)
(*context.listenCount)++;
}
});
}
@@ -303,7 +444,7 @@ namespace Scrobbling::ListenBrainz
void
ListensSynchronizer::startGetListens()
{
LOG(DEBUG) << "GetListens started!!!";
LOG(DEBUG) << "GetListens started!";
assert(!isFetching());
@@ -26,6 +26,8 @@
#include <boost/asio/steady_timer.hpp>
#include "services/database/Types.hpp"
#include "services/database/ListenId.hpp"
#include "services/database/UserId.hpp"
#include "services/scrobbling/Listen.hpp"
namespace Database
@@ -48,9 +50,12 @@ namespace Scrobbling::ListenBrainz
public:
ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, Http::IClient& client);
void saveListen(const TimedListen& listen);
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
private:
Database::ListenId saveListen(const TimedListen& listen);
void onListenSent(Database::ListenId listenId);
struct UserContext
{
UserContext(Database::UserId id) : userId {id} {}