Auto reformatted the base, ref #470

This commit is contained in:
emeric
2024-05-24 23:31:52 +02:00
parent 83b868673c
commit 39941d90a3
460 changed files with 8583 additions and 8514 deletions
@@ -30,7 +30,7 @@ namespace lms::db
class Session;
class TrackList;
class User;
}
} // namespace lms::db
namespace lms::scrobbling
{
@@ -44,5 +44,4 @@ namespace lms::scrobbling
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0;
};
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -19,6 +19,7 @@
#include "ScrobblingService.hpp"
#include "core/ILogger.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Listen.hpp"
@@ -26,7 +27,6 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/User.hpp"
#include "core/ILogger.hpp"
#include "internal/InternalBackend.hpp"
#include "listenbrainz/ListenBrainzBackend.hpp"
@@ -54,7 +54,7 @@ namespace lms::scrobbling
{
return db::Listen::ArtistStatsFindParameters{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType };
}
}
} // namespace
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
{
@@ -253,5 +253,4 @@ namespace lms::scrobbling
res = db::Listen::getTopTracks(session, listenFindParams);
return res;
}
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -24,6 +24,7 @@
#include <unordered_map>
#include "services/scrobbling/IScrobblingService.hpp"
#include "IScrobblingBackend.hpp"
namespace lms::scrobbling
@@ -59,5 +60,4 @@ namespace lms::scrobbling
std::unordered_map<db::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
};
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -29,7 +29,8 @@ namespace lms::scrobbling
{
InternalBackend::InternalBackend(db::Db& db)
: _db{ db }
{}
{
}
void InternalBackend::listenStarted(const Listen&)
{
@@ -64,5 +65,4 @@ namespace lms::scrobbling
auto dbListen{ session.create<db::Listen>(user, track, db::ScrobblingBackend::Internal, listen.listenedAt) };
dbListen.modify()->setSyncState(db::SyncState::Synchronized);
}
} // Scrobbling
} // namespace lms::scrobbling
@@ -40,5 +40,4 @@ namespace lms::scrobbling
db::Db& _db;
};
} // Scrobbling
} // namespace lms::scrobbling
@@ -23,9 +23,9 @@
namespace lms::scrobbling::listenBrainz
{
class Exception : public scrobbling::Exception
{
public:
using scrobbling::Exception::Exception;
};
}
class Exception : public scrobbling::Exception
{
public:
using scrobbling::Exception::Exception;
};
} // namespace lms::scrobbling::listenBrainz
@@ -19,13 +19,14 @@
#include "ListenBrainzBackend.hpp"
#include "core/IConfig.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "core/http/IClient.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "core/IConfig.hpp"
#include "core/http/IClient.hpp"
#include "core/ILogger.hpp"
#include "core/Service.hpp"
#include "Utils.hpp"
namespace lms::scrobbling::listenBrainz
@@ -48,7 +49,7 @@ namespace lms::scrobbling::listenBrainz
return res;
}
}
} // namespace
ListenBrainzBackend::ListenBrainzBackend(boost::asio::io_context& ioContext, Db& db)
: _ioContext{ ioContext }
@@ -84,4 +85,3 @@ namespace lms::scrobbling::listenBrainz
_listensSynchronizer.enqueListen(timedListen);
}
} // namespace lms::scrobbling::listenBrainz
@@ -19,8 +19,9 @@
#pragma once
#include <string>
#include <optional>
#include <string>
#include <boost/asio/io_context.hpp>
#include "IScrobblingBackend.hpp"
@@ -50,11 +51,10 @@ namespace lms::scrobbling::listenBrainz
// Submit listens
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
boost::asio::io_context& _ioContext;
db::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
boost::asio::io_context& _ioContext;
db::Db& _db;
std::string _baseAPIUrl;
std::unique_ptr<core::http::IClient> _client;
ListensSynchronizer _listensSynchronizer;
};
} // scrobbling::ListenBrainz
} // namespace lms::scrobbling::listenBrainz
@@ -21,21 +21,20 @@
namespace lms::scrobbling::listenBrainz
{
std::ostream&
operator<<(std::ostream& os, const Listen& listen)
{
os << "track name = '" << listen.trackName << "', artistName = '" << listen.artistName << "'";
if (listen.listenedAt.isValid())
os << ", listenedAt = " << listen.listenedAt.toString();
if (!listen.releaseName.empty())
os << ", releaseName = '" << listen.releaseName << "'";
if (listen.trackNumber)
os << ", trackNumber = " << *listen.trackNumber;
if (listen.trackMBID)
os << ", trackMBID = '" << listen.trackMBID->getAsString() << "'";
if (listen.recordingMBID)
os << ", recordingMBID = '" << listen.recordingMBID->getAsString() << "'";
std::ostream& operator<<(std::ostream& os, const Listen& listen)
{
os << "track name = '" << listen.trackName << "', artistName = '" << listen.artistName << "'";
if (listen.listenedAt.isValid())
os << ", listenedAt = " << listen.listenedAt.toString();
if (!listen.releaseName.empty())
os << ", releaseName = '" << listen.releaseName << "'";
if (listen.trackNumber)
os << ", trackNumber = " << *listen.trackNumber;
if (listen.trackMBID)
os << ", trackMBID = '" << listen.trackMBID->getAsString() << "'";
if (listen.recordingMBID)
os << ", recordingMBID = '" << listen.recordingMBID->getAsString() << "'";
return os;
}
} // scrobbling::ListenBrainz
return os;
}
} // namespace lms::scrobbling::listenBrainz
@@ -19,25 +19,26 @@
#pragma once
#include <string>
#include <ostream>
#include <string>
#include <Wt/WDateTime.h>
#include "core/UUID.hpp"
namespace lms::scrobbling::listenBrainz
{
struct Listen
{
std::string trackName;
std::string releaseName;
std::string artistName;
std::optional<core::UUID> recordingMBID;
std::optional<core::UUID> trackMBID;
std::optional<core::UUID> releaseMBID;
std::optional<unsigned> trackNumber;
Wt::WDateTime listenedAt;
};
struct Listen
{
std::string trackName;
std::string releaseName;
std::string artistName;
std::optional<core::UUID> recordingMBID;
std::optional<core::UUID> trackMBID;
std::optional<core::UUID> releaseMBID;
std::optional<unsigned> trackNumber;
Wt::WDateTime listenedAt;
};
std::ostream& operator<<(std::ostream& os, const Listen& listen);
}
std::ostream& operator<<(std::ostream& os, const Listen& listen);
} // namespace lms::scrobbling::listenBrainz
@@ -21,10 +21,11 @@
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
#include <Wt/Json/Value.h>
#include "core/ILogger.hpp"
#include "Utils.hpp"
namespace lms::scrobbling::listenBrainz
@@ -102,4 +103,4 @@ namespace lms::scrobbling::listenBrainz
return result;
}
} // scrobbling::ListenBrainz
} // namespace lms::scrobbling::listenBrainz
@@ -25,15 +25,15 @@
namespace lms::scrobbling::listenBrainz
{
class ListensParser
{
public:
struct Result
{
std::size_t listenCount {}; // may be > than listens.size()
std::vector<Listen> listens; // successfully parsed listens
};
class ListensParser
{
public:
struct Result
{
std::size_t listenCount{}; // may be > than listens.size()
std::vector<Listen> listens; // successfully parsed listens
};
static Result parse(std::string_view msgBody);
};
} // scrobbling::ListenBrainz
static Result parse(std::string_view msgBody);
};
} // namespace lms::scrobbling::listenBrainz
@@ -19,12 +19,16 @@
#include "ListensSynchronizer.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 <Wt/Json/Value.h>
#include <boost/asio/bind_executor.hpp>
#include "ListensParser.hpp"
#include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/http/IClient.hpp"
#include "database/Artist.hpp"
#include "database/Db.hpp"
#include "database/Listen.hpp"
@@ -33,10 +37,6 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "services/scrobbling/Exception.hpp"
#include "core/IConfig.hpp"
#include "core/http/IClient.hpp"
#include "core/Service.hpp"
#include "ListensParser.hpp"
#include "Utils.hpp"
@@ -52,7 +52,7 @@ namespace lms::scrobbling::listenBrainz
if (!track)
return std::nullopt;
auto artists{ track->getArtists({db::TrackArtistLinkType::Artist}) };
auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) };
if (artists.empty())
artists = track->getArtists({ db::TrackArtistLinkType::ReleaseArtist });
@@ -65,12 +65,12 @@ namespace lms::scrobbling::listenBrainz
Wt::Json::Object additionalInfo;
additionalInfo["listening_from"] = "LMS";
additionalInfo["duration_ms"] = std::chrono::duration_cast<std::chrono::milliseconds>(track->getDuration()).count();
if (const auto release {track->getRelease()})
if (const auto release{ track->getRelease() })
{
if (auto MBID{ release->getMBID() })
additionalInfo["release_mbid"] = Wt::Json::Value{ std::string {MBID->getAsString()} };
additionalInfo["release_mbid"] = Wt::Json::Value{ std::string{ MBID->getAsString() } };
if (auto groupMBID{ release->getGroupMBID() })
additionalInfo["release_group_mbid"] = Wt::Json::Value{ std::string {groupMBID->getAsString()} };
additionalInfo["release_group_mbid"] = Wt::Json::Value{ std::string{ groupMBID->getAsString() } };
}
{
@@ -78,7 +78,7 @@ namespace lms::scrobbling::listenBrainz
for (const db::Artist::pointer& artist : artists)
{
if (auto MBID{ artist->getMBID() })
artistMBIDs.push_back(Wt::Json::Value{ std::string {MBID->getAsString()} });
artistMBIDs.push_back(Wt::Json::Value{ std::string{ MBID->getAsString() } });
}
if (!artistMBIDs.empty())
@@ -86,10 +86,10 @@ namespace lms::scrobbling::listenBrainz
}
if (auto MBID{ track->getTrackMBID() })
additionalInfo["track_mbid"] = Wt::Json::Value{ std::string {MBID->getAsString()} };
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()} };
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) };
@@ -99,7 +99,7 @@ namespace lms::scrobbling::listenBrainz
trackMetadata["artist_name"] = Wt::Json::Value{ std::string{ track->getArtistDisplayName() } };
trackMetadata["track_name"] = Wt::Json::Value{ track->getName() };
if (track->getRelease())
trackMetadata["release_name"] = Wt::Json::Value{ std::string {track->getRelease()->getName()} };
trackMetadata["release_name"] = Wt::Json::Value{ std::string{ track->getRelease()->getName() } };
Wt::Json::Object payload;
payload["track_metadata"] = std::move(trackMetadata);
@@ -118,7 +118,7 @@ namespace lms::scrobbling::listenBrainz
return res;
Wt::Json::Object root;
root["listen_type"] = Wt::Json::Value{ std::string {listenType} };
root["listen_type"] = Wt::Json::Value{ std::string{ listenType } };
root["payload"] = Wt::Json::Array{ std::move(*payload) };
res = Wt::Json::serialize(root);
@@ -207,7 +207,7 @@ namespace lms::scrobbling::listenBrainz
LOG(DEBUG, "No match for listen '" << listen << "'");
return {};
}
}
} // namespace
ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, db::Db& db, core::http::IClient& client)
: _ioContext{ ioContext }
@@ -244,18 +244,16 @@ namespace lms::scrobbling::listenBrainz
saveListen(timedListen, db::SyncState::PendingAdd);
request.priority = core::http::ClientRequestParameters::Priority::Normal;
request.onSuccessFunc = [this, timedListen](std::string_view)
{
_strand.dispatch([this, timedListen]
{
if (saveListen(timedListen, db::SyncState::Synchronized))
{
UserContext& context{ getUserContext(timedListen.userId) };
if (context.listenCount)
(*context.listenCount)++;
}
});
};
request.onSuccessFunc = [this, timedListen](std::string_view) {
_strand.dispatch([this, timedListen] {
if (saveListen(timedListen, db::SyncState::Synchronized))
{
UserContext& context{ getUserContext(timedListen.userId) };
if (context.listenCount)
(*context.listenCount)++;
}
});
};
// on failure, this listen will be sent during the next sync
}
else
@@ -369,10 +367,9 @@ namespace lms::scrobbling::listenBrainz
bool ListensSynchronizer::isSyncing() const
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
return contextEntry.second.syncing;
});
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry) {
return contextEntry.second.syncing;
});
}
void ListensSynchronizer::scheduleSync(std::chrono::seconds fromNow)
@@ -382,20 +379,19 @@ namespace lms::scrobbling::listenBrainz
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)
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec) {
if (ec == boost::asio::error::operation_aborted)
{
if (ec == boost::asio::error::operation_aborted)
{
LOG(DEBUG, "getListens aborted");
return;
}
else if (ec)
{
throw Exception{ "GetListens timer failure: " + std::string {ec.message()} };
}
LOG(DEBUG, "getListens aborted");
return;
}
else if (ec)
{
throw Exception{ "GetListens timer failure: " + std::string{ ec.message() } };
}
startSync();
}));
startSync();
}));
}
void ListensSynchronizer::startSync()
@@ -434,14 +430,13 @@ namespace lms::scrobbling::listenBrainz
void ListensSynchronizer::onSyncEnded(UserContext& context)
{
_strand.dispatch([this, &context]
{
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
context.syncing = false;
_strand.dispatch([this, &context] {
LOG(INFO, "Sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount);
context.syncing = false;
if (!isSyncing())
scheduleSync(_syncListensPeriod);
});
if (!isSyncing())
scheduleSync(_syncListensPeriod);
});
}
void ListensSynchronizer::enqueValidateToken(UserContext& context)
@@ -458,21 +453,19 @@ namespace lms::scrobbling::listenBrainz
core::http::ClientGETRequestParameters request;
request.priority = core::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;
}
enqueGetListenCount(context);
};
request.onFailureFunc = [this, &context]
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;
}
enqueGetListenCount(context);
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -484,31 +477,28 @@ namespace lms::scrobbling::listenBrainz
core::http::ClientGETRequestParameters request;
request.relativeUrl = "/1/user/" + std::string{ context.listenBrainzUserName } + "/listen-count";
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
const auto listenCount{ parseListenCount(msgBody) };
_strand.dispatch([this, listenCount, &context]
{
if (listenCount)
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
const auto listenCount{ parseListenCount(msgBody) };
_strand.dispatch([this, listenCount, &context] {
if (listenCount)
LOG(DEBUG, "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount);
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
context.listenCount = listenCount;
bool needSync{ listenCount && (!context.listenCount || *context.listenCount != *listenCount) };
context.listenCount = listenCount;
if (!needSync)
{
onSyncEnded(context);
return;
}
if (!needSync)
{
onSyncEnded(context);
return;
}
context.maxDateTime = Wt::WDateTime::currentDateTime();
enqueGetListens(context);
});
};
request.onFailureFunc = [this, &context]
{
onSyncEnded(context);
};
context.maxDateTime = Wt::WDateTime::currentDateTime();
enqueGetListens(context);
});
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -520,21 +510,19 @@ namespace lms::scrobbling::listenBrainz
core::http::ClientGETRequestParameters request;
request.relativeUrl = "/1/user/" + context.listenBrainzUserName + "/listens?max_ts=" + std::to_string(context.maxDateTime.toTime_t());
request.priority = core::http::ClientRequestParameters::Priority::Low;
request.onSuccessFunc = [this, &context](std::string_view msgBody)
{
processGetListensResponse(msgBody, context);
if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid())
{
onSyncEnded(context);
return;
}
enqueGetListens(context);
};
request.onFailureFunc = [this, &context]
request.onSuccessFunc = [this, &context](std::string_view msgBody) {
processGetListensResponse(msgBody, context);
if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid())
{
onSyncEnded(context);
};
return;
}
enqueGetListens(context);
};
request.onFailureFunc = [this, &context] {
onSyncEnded(context);
};
_client.sendGETRequest(std::move(request));
}
@@ -563,7 +551,7 @@ namespace lms::scrobbling::listenBrainz
{
context.matchedListenCount++;
const scrobbling::TimedListen listen{ {context.userId, trackId}, parsedListen.listenedAt };
const scrobbling::TimedListen listen{ { context.userId, trackId }, parsedListen.listenedAt };
if (saveListen(listen, db::SyncState::Synchronized))
context.importedListenCount++;
}
@@ -21,6 +21,7 @@
#include <optional>
#include <unordered_map>
#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/steady_timer.hpp>
@@ -32,74 +33,74 @@
namespace lms
{
namespace core::http
{
class IClient;
}
namespace db
{
class Db;
}
}
namespace core::http
{
class IClient;
}
namespace db
{
class Db;
}
} // namespace lms
namespace lms::scrobbling::listenBrainz
{
class ListensSynchronizer
{
public:
ListensSynchronizer(boost::asio::io_context& ioContext, db::Db& db, core::http::IClient& client);
class ListensSynchronizer
{
public:
ListensSynchronizer(boost::asio::io_context& ioContext, db::Db& db, core::http::IClient& client);
void enqueListen(const TimedListen& listen);
void enqueListenNow(const Listen& listen);
void enqueListen(const TimedListen& listen);
void enqueListenNow(const Listen& listen);
private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
bool saveListen(const TimedListen& listen, db::SyncState scrobblinState);
private:
void enqueListen(const Listen& listen, const Wt::WDateTime& timePoint);
bool saveListen(const TimedListen& listen, db::SyncState scrobblinState);
void enquePendingListens();
void enquePendingListens();
struct UserContext
{
UserContext(db::UserId id) : userId {id} {}
struct UserContext
{
UserContext(db::UserId id)
: userId{ id } {}
UserContext(const UserContext&) = delete;
UserContext(UserContext&&) = delete;
UserContext& operator=(const UserContext&) = delete;
UserContext& operator=(UserContext&&) = delete;
UserContext(const UserContext&) = delete;
UserContext(UserContext&&) = delete;
UserContext& operator=(const UserContext&) = delete;
UserContext& operator=(UserContext&&) = delete;
const db::UserId userId;
bool syncing {};
std::optional<std::size_t> listenCount {};
const db::UserId userId;
bool syncing{};
std::optional<std::size_t> listenCount{};
// resetted at each sync
std::string listenBrainzUserName; // need to be resolved first
Wt::WDateTime maxDateTime;
std::size_t fetchedListenCount{};
std::size_t matchedListenCount{};
std::size_t importedListenCount{};
};
// resetted at each sync
std::string listenBrainzUserName; // need to be resolved first
Wt::WDateTime maxDateTime;
std::size_t fetchedListenCount{};
std::size_t matchedListenCount{};
std::size_t importedListenCount{};
};
UserContext& getUserContext(db::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 enqueGetListenCount(UserContext& context);
void enqueGetListens(UserContext& context);
void processGetListensResponse(std::string_view body, UserContext& context);
UserContext& getUserContext(db::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 enqueGetListenCount(UserContext& context);
void enqueGetListens(UserContext& context);
void processGetListensResponse(std::string_view body, UserContext& context);
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand {_ioContext};
db::Db& _db;
boost::asio::steady_timer _syncTimer {_ioContext};
core::http::IClient& _client;
boost::asio::io_context& _ioContext;
boost::asio::io_context::strand _strand{ _ioContext };
db::Db& _db;
boost::asio::steady_timer _syncTimer{ _ioContext };
core::http::IClient& _client;
std::unordered_map<db::UserId, UserContext> _userContexts;
const std::size_t _maxSyncListenCount;
const std::chrono::hours _syncListensPeriod;
};
} // scrobbling::ListenBrainz
std::unordered_map<db::UserId, UserContext> _userContexts;
const std::size_t _maxSyncListenCount;
const std::chrono::hours _syncListensPeriod;
};
} // namespace lms::scrobbling::listenBrainz
@@ -59,4 +59,4 @@ namespace lms::scrobbling::listenBrainz::utils
listenBrainzUserName = root.get("user_name").orIfNull("");
return listenBrainzUserName;
}
}
} // namespace lms::scrobbling::listenBrainz::utils
@@ -19,11 +19,11 @@
#pragma once
#include "database/UserId.hpp"
#include "core/ILogger.hpp"
#include "core/UUID.hpp"
#include "database/UserId.hpp"
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[listenbrainz] " << message)
#define LOG(sev, message) LMS_LOG(SCROBBLING, sev, "[listenbrainz] " << message)
namespace lms::db
{
@@ -32,6 +32,6 @@ namespace lms::db
namespace lms::scrobbling::listenBrainz::utils
{
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::optional<core::UUID> getListenBrainzToken(db::Session& session, db::UserId userId);
std::string parseValidateToken(std::string_view msgBody);
}
} // namespace lms::scrobbling::listenBrainz::utils
@@ -28,4 +28,4 @@ namespace lms::scrobbling
public:
using LmsException::LmsException;
};
}
} // namespace lms::scrobbling
@@ -23,16 +23,17 @@
#include <memory>
#include <optional>
#include <span>
#include <boost/asio/io_service.hpp>
#include <Wt/WDateTime.h>
#include "services/scrobbling/Listen.hpp"
#include <Wt/WDateTime.h>
#include <boost/asio/io_service.hpp>
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "database/Types.hpp"
#include "services/scrobbling/Listen.hpp"
namespace lms::db
{
@@ -59,29 +60,61 @@ namespace lms::scrobbling
struct FindParameters
{
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library; // if set, match this library
db::ArtistId artist; // if set, match this artist
db::UserId user;
std::vector<db::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::optional<db::Range> range;
db::MediaLibraryId library; // if set, match this library
db::ArtistId artist; // if set, match this artist
FindParameters& setUser(const db::UserId _user) { user = _user; return *this; }
FindParameters& setClusters(std::span<const db::ClusterId> _clusters) { clusters.assign(std::cbegin(_clusters), std::cend(_clusters)); return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setRange(std::optional<db::Range> _range) { range = _range; return *this; }
FindParameters& setMediaLibrary(db::MediaLibraryId _library) { library = _library; return *this; }
FindParameters& setArtist(db::ArtistId _artist) { artist = _artist; return *this; }
FindParameters& setUser(const db::UserId _user)
{
user = _user;
return *this;
}
FindParameters& setClusters(std::span<const db::ClusterId> _clusters)
{
clusters.assign(std::cbegin(_clusters), std::cend(_clusters));
return *this;
}
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
{
keywords = _keywords;
return *this;
}
FindParameters& setRange(std::optional<db::Range> _range)
{
range = _range;
return *this;
}
FindParameters& setMediaLibrary(db::MediaLibraryId _library)
{
library = _library;
return *this;
}
FindParameters& setArtist(db::ArtistId _artist)
{
artist = _artist;
return *this;
}
};
// Artists
struct ArtistFindParameters : public FindParameters
{
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType)
{
linkType = _linkType;
return *this;
}
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
};
// From most recent to oldest
@@ -102,4 +135,4 @@ namespace lms::scrobbling
};
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, db::Db& db);
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -36,5 +36,4 @@ namespace lms::scrobbling
{
Wt::WDateTime listenedAt;
};
} // ns Scrobbling
} // namespace lms::scrobbling
@@ -25,7 +25,7 @@ namespace lms::scrobbling::listenBrainz::tests
{
TEST(Listenbrainz, parseListens_empty)
{
ListensParser::Result result {ListensParser::parse("")};
ListensParser::Result result{ ListensParser::parse("") };
EXPECT_EQ(result.listenCount, 0);
EXPECT_EQ(result.listens.size(), 0);
@@ -36,7 +36,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_single_missingMBID)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1663159479,"listens":[{"inserted_at":1650541124,"listened_at":1650541124,"recording_msid":"0e1418e3-b485-413a-84af-6316312cb116","track_metadata":{"additional_info":{"artist_msid":"ab5b27ad-e579-441c-ac60-d5dd9975c044","listening_from":"LMS","recording_msid":"0e1418e3-b485-413a-84af-6316312cb116","release_msid":"3f22f274-a9ee-4cb2-8dd1-f3bd18407099","tracknumber":8},"artist_name":"Broke For Free","release_name":"YEKOMS","track_name":"U2B"},"user_name":"epoupon"}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1663159479,"listens":[{"inserted_at":1650541124,"listened_at":1650541124,"recording_msid":"0e1418e3-b485-413a-84af-6316312cb116","track_metadata":{"additional_info":{"artist_msid":"ab5b27ad-e579-441c-ac60-d5dd9975c044","listening_from":"LMS","recording_msid":"0e1418e3-b485-413a-84af-6316312cb116","release_msid":"3f22f274-a9ee-4cb2-8dd1-f3bd18407099","tracknumber":8},"artist_name":"Broke For Free","release_name":"YEKOMS","track_name":"U2B"},"user_name":"epoupon"}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 1);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_EQ(result.listens[0].trackName, "U2B");
@@ -53,7 +53,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_tworesults)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":2,"latest_listen_ts":1664028167,"listens":[{"inserted_at":1664028167,"listened_at":1664028167,"recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"5427a943-a096-4d0b-8b9a-53aca9ed61ac","tracknumber":5},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Juparo"},"user_name":"epoupon"},{"inserted_at":1664027919,"listened_at":1664027918,"recording_msid":"fe5abc47-89cd-4235-80b5-00f47cecbe01","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"d89d042c-8cc1-4526-9080-5bab728ee15f","recording_msid":"fe5abc47-89cd-4235-80b5-00f47cecbe01","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"9f33a17f-e33e-492f-85a4-7b2e9e09613e","tracknumber":4},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"d89d042c-8cc1-4526-9080-5bab728ee15f","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Melt"},"user_name":"epoupon"}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":2,"latest_listen_ts":1664028167,"listens":[{"inserted_at":1664028167,"listened_at":1664028167,"recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"5427a943-a096-4d0b-8b9a-53aca9ed61ac","tracknumber":5},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Juparo"},"user_name":"epoupon"},{"inserted_at":1664027919,"listened_at":1664027918,"recording_msid":"fe5abc47-89cd-4235-80b5-00f47cecbe01","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"d89d042c-8cc1-4526-9080-5bab728ee15f","recording_msid":"fe5abc47-89cd-4235-80b5-00f47cecbe01","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"9f33a17f-e33e-492f-85a4-7b2e9e09613e","tracknumber":4},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"d89d042c-8cc1-4526-9080-5bab728ee15f","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Melt"},"user_name":"epoupon"}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 2);
ASSERT_EQ(result.listens.size(), 2);
EXPECT_EQ(result.listens[0].trackName, "Juparo");
@@ -77,7 +77,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_tworesults_butinvalid)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":2,"latest_listen_ts":1664028167,"listens":[{"inserted_at":1664028167,"listened_at":1664028167,"recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"5427a943-a096-4d0b-8b9a-53aca9ed61ac","tracknumber":5},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Juparo"},"user_name":"epoupon"},{"inserted_at":1664027919}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":2,"latest_listen_ts":1664028167,"listens":[{"inserted_at":1664028167,"listened_at":1664028167,"recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","track_metadata":{"additional_info":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","recording_msid":"29c11137-e40b-4875-9ec0-9a20a4bdc2d3","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc","release_msid":null,"track_mbid":"5427a943-a096-4d0b-8b9a-53aca9ed61ac","tracknumber":5},"artist_name":"Broke For Free","mbid_mapping":{"artist_mbids":["069a1c1f-14eb-4d36-b0a0-77dffbd67713"],"recording_mbid":"46ae879f-2dbe-46d3-99ad-05c116f97a30","release_mbid":"44915500-fbb9-4060-98ce-59a57a429edc"},"release_name":"Petal","track_name":"Juparo"},"user_name":"epoupon"},{"inserted_at":1664027919}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 2);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_EQ(result.listens[0].trackName, "Juparo");
@@ -92,7 +92,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_entryNotFromLms)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664105730,"listens":[{"inserted_at":1664105730,"listened_at":1664105730,"recording_msid":"6a11ff4d-0623-4b2e-98e0-0e172f1f28d7","track_metadata":{"additional_info":{"artist_msid":null,"media_player":"BrainzPlayer","music_service":"youtube.com","music_service_name":"youtube","origin_url":"https://www.youtube.com/watch?v=EBP5vL3YWTI","recording_msid":"6a11ff4d-0623-4b2e-98e0-0e172f1f28d7","release_msid":null,"submission_client":"BrainzPlayer"},"artist_name":"Dio","brainzplayer_metadata":{"track_name":"Dio - Breathless"},"mbid_mapping":{"artist_mbids":["c55193fb-f5d2-4839-a263-4c044fca1456"],"recording_mbid":"92929526-21d7-4e75-b759-1072951664c4","release_mbid":"16cbf9ba-2e38-3893-9f23-f8567e26c18b"},"release_name":"The Last in Line","track_name":"Breathless"},"user_name":"epoupon"}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664105730,"listens":[{"inserted_at":1664105730,"listened_at":1664105730,"recording_msid":"6a11ff4d-0623-4b2e-98e0-0e172f1f28d7","track_metadata":{"additional_info":{"artist_msid":null,"media_player":"BrainzPlayer","music_service":"youtube.com","music_service_name":"youtube","origin_url":"https://www.youtube.com/watch?v=EBP5vL3YWTI","recording_msid":"6a11ff4d-0623-4b2e-98e0-0e172f1f28d7","release_msid":null,"submission_client":"BrainzPlayer"},"artist_name":"Dio","brainzplayer_metadata":{"track_name":"Dio - Breathless"},"mbid_mapping":{"artist_mbids":["c55193fb-f5d2-4839-a263-4c044fca1456"],"recording_mbid":"92929526-21d7-4e75-b759-1072951664c4","release_mbid":"16cbf9ba-2e38-3893-9f23-f8567e26c18b"},"release_name":"The Last in Line","track_name":"Breathless"},"user_name":"epoupon"}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 1);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_EQ(result.listens[0].trackName, "Breathless");
@@ -104,7 +104,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_multiArtists)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664106427,"listens":[{"inserted_at":1664106427,"listened_at":1664106427,"recording_msid":"b1dad0df-329b-443d-bacf-cdbebdddbfd0","track_metadata":{"additional_info":{"artist_mbids":["04ce0202-043d-4cbe-8f09-8abaf3b80c71","79311c51-9748-49df-baa1-d925fd29f4e8"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"a5f380bc-0a85-4a9f-88db-d41bb9aa2a4b","recording_msid":"b1dad0df-329b-443d-bacf-cdbebdddbfd0","release_mbid":"147b4669-3d20-43f8-89c0-ba1da8b87dd3","release_msid":null,"track_mbid":"a20dd067-29b6-3d38-a0be-eeb86b4671c1","tracknumber":1},"artist_name":"Gloom","release_name":"Demovibes 9: Party, people going","track_name":"Stargazer of Disgrace"},"user_name":"epoupon"}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664106427,"listens":[{"inserted_at":1664106427,"listened_at":1664106427,"recording_msid":"b1dad0df-329b-443d-bacf-cdbebdddbfd0","track_metadata":{"additional_info":{"artist_mbids":["04ce0202-043d-4cbe-8f09-8abaf3b80c71","79311c51-9748-49df-baa1-d925fd29f4e8"],"artist_msid":null,"listening_from":"LMS","recording_mbid":"a5f380bc-0a85-4a9f-88db-d41bb9aa2a4b","recording_msid":"b1dad0df-329b-443d-bacf-cdbebdddbfd0","release_mbid":"147b4669-3d20-43f8-89c0-ba1da8b87dd3","release_msid":null,"track_mbid":"a20dd067-29b6-3d38-a0be-eeb86b4671c1","tracknumber":1},"artist_name":"Gloom","release_name":"Demovibes 9: Party, people going","track_name":"Stargazer of Disgrace"},"user_name":"epoupon"}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 1);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_EQ(result.listens[0].trackNumber, 1);
@@ -112,7 +112,7 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_minPayload)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664106427,"listens":[{"track_metadata":{"artist_name":"Gloom","track_name":"Stargazer of Disgrace"},"user_name":"epoupon"}],"user_id":"epoupon"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1664106427,"listens":[{"track_metadata":{"artist_name":"Gloom","track_name":"Stargazer of Disgrace"},"user_name":"epoupon"}],"user_id":"epoupon"}})") };
EXPECT_EQ(result.listenCount, 1);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_FALSE(result.listens[0].listenedAt.isValid());
@@ -126,10 +126,10 @@ namespace lms::scrobbling::listenBrainz::tests
TEST(Listenbrainz, parseListens_stringInsteadOfInt)
{
ListensParser::Result result {ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1683298766,"listens":[{"inserted_at":1683299849,"listened_at":1683298766,"recording_msid":"3841ad1c-f674-491c-b855-f10a82ea38fe","track_metadata":{"additional_info":{"albumartist":"Various Artists","artist_mbids":["80ccfede-c258-4575-a7ad-c982e9932e0f"],"date":"2002-06-10","discnumber":"1","duration":190,"isrc":"USAM10200204","media_player":"foobar2000","media_player_version":"1.6.16","recording_msid":"3841ad1c-f674-491c-b855-f10a82ea38fe","release_group_mbid":"25f44677-2ecd-33fb-8454-9a9776496e3c","release_mbid":"bddad4c6-e9e1-4218-b9d2-b02a5a778a7d","submission_client":"foobar2000","submission_client_version":"1.3.2","totaldiscs":"1","totaltracks":"18","track_mbid":"08b1a5c1-c845-3d3b-9aa4-560a1af7f28c","tracknumber":"4"},"artist_name":"Sheryl Crow","mbid_mapping":{"artist_mbids":["80ccfede-c258-4575-a7ad-c982e9932e0f"],"artists":[{"artist_credit_name":"Sheryl Crow","artist_mbid":"80ccfede-c258-4575-a7ad-c982e9932e0f","join_phrase":""}],"caa_id":22039936498,"caa_release_mbid":"f10b4b40-4db1-4b9a-a223-06915fa06b3c","recording_mbid":"7ca21c82-1345-44df-b7ac-035a6e05d4ed","recording_name":"Strong Enough","release_mbid":"f10b4b40-4db1-4b9a-a223-06915fa06b3c"},"release_name":"The Very Best of MTV Unplugged","track_name":"Strong Enough"},"user_name":"awesomeuser"}],"user_id":"awesomeuser"}})")};
ListensParser::Result result{ ListensParser::parse(R"({"payload":{"count":1,"latest_listen_ts":1683298766,"listens":[{"inserted_at":1683299849,"listened_at":1683298766,"recording_msid":"3841ad1c-f674-491c-b855-f10a82ea38fe","track_metadata":{"additional_info":{"albumartist":"Various Artists","artist_mbids":["80ccfede-c258-4575-a7ad-c982e9932e0f"],"date":"2002-06-10","discnumber":"1","duration":190,"isrc":"USAM10200204","media_player":"foobar2000","media_player_version":"1.6.16","recording_msid":"3841ad1c-f674-491c-b855-f10a82ea38fe","release_group_mbid":"25f44677-2ecd-33fb-8454-9a9776496e3c","release_mbid":"bddad4c6-e9e1-4218-b9d2-b02a5a778a7d","submission_client":"foobar2000","submission_client_version":"1.3.2","totaldiscs":"1","totaltracks":"18","track_mbid":"08b1a5c1-c845-3d3b-9aa4-560a1af7f28c","tracknumber":"4"},"artist_name":"Sheryl Crow","mbid_mapping":{"artist_mbids":["80ccfede-c258-4575-a7ad-c982e9932e0f"],"artists":[{"artist_credit_name":"Sheryl Crow","artist_mbid":"80ccfede-c258-4575-a7ad-c982e9932e0f","join_phrase":""}],"caa_id":22039936498,"caa_release_mbid":"f10b4b40-4db1-4b9a-a223-06915fa06b3c","recording_mbid":"7ca21c82-1345-44df-b7ac-035a6e05d4ed","recording_name":"Strong Enough","release_mbid":"f10b4b40-4db1-4b9a-a223-06915fa06b3c"},"release_name":"The Very Best of MTV Unplugged","track_name":"Strong Enough"},"user_name":"awesomeuser"}],"user_id":"awesomeuser"}})") };
EXPECT_EQ(result.listenCount, 1);
ASSERT_EQ(result.listens.size(), 1);
EXPECT_EQ(result.listens[0].trackNumber, 4);
}
}
} // namespace lms::scrobbling::listenBrainz::tests
@@ -27,9 +27,8 @@ int main(int argc, char** argv)
{
using namespace lms;
// log to stdout
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout, core::EnumSet<core::logging::Severity> {core::logging::Severity::FATAL, core::logging::Severity::ERROR}) };
core::Service<core::logging::ILogger> logger{ std::make_unique<core::logging::StreamLogger>(std::cout, core::EnumSet<core::logging::Severity>{ core::logging::Severity::FATAL, core::logging::Severity::ERROR }) };
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}