OpenSubsonic API: added support for 'played' fields

This commit is contained in:
emeric
2023-10-16 23:27:29 +02:00
parent 6a3783a4ba
commit 079f7922b4
9 changed files with 1042 additions and 938 deletions
+2
View File
@@ -11,6 +11,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
## Extra fields ## Extra fields
The following extra fields are implemented: The following extra fields are implemented:
* `Album` response: * `Album` response:
* `played`
* `musicBrainzId` * `musicBrainzId`
* `genres` * `genres`
* `artists` * `artists`
@@ -21,6 +22,7 @@ The following extra fields are implemented:
* `isCompilation` * `isCompilation`
* `discTitles`: discs with no subtitle are omitted * `discTitles`: discs with no subtitle are omitted
* `Child` response: * `Child` response:
* `played`
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
* `genres` * `genres`
* `artists` * `artists`
+191 -207
View File
@@ -27,261 +27,245 @@
namespace namespace
{ {
using namespace Database; using namespace Database;
Wt::Dbo::Query<ArtistId> Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, 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.scrobbler = ?").bind(scrobbler)};
if (linkType) if (linkType)
query.where("t_a_l.type = ?").bind(*linkType); query.where("t_a_l.type = ?").bind(*linkType);
if (!clusterIds.empty()) if (!clusterIds.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id" " INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; WhereClause clusterClause;
for (auto id : clusterIds) for (auto id : clusterIds)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id); query.bind(id);
} }
oss << " " << clusterClause.get(); oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
return query; return query;
} }
Wt::Dbo::Query<ReleaseId> Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, 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.scrobbler = ?").bind(scrobbler)};
if (!clusterIds.empty()) if (!clusterIds.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r" oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id" " INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; WhereClause clusterClause;
for (ClusterId id : clusterIds) for (ClusterId id : clusterIds)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id); query.bind(id);
} }
oss << " " << clusterClause.get(); oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
return query; return query;
} }
Wt::Dbo::Query<TrackId> Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, 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.scrobbler = ?").bind(scrobbler)};
if (!clusterIds.empty()) if (!clusterIds.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "t.id IN (SELECT DISTINCT t.id FROM track t" oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"; " INNER JOIN cluster c ON c.id = t_c.cluster_id";
WhereClause clusterClause; WhereClause clusterClause;
for (auto id : clusterIds) for (auto id : clusterIds)
{ {
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString()); clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
query.bind(id); query.bind(id);
} }
oss << " " << clusterClause.get(); oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
return query; return query;
} }
} }
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, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} : _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
, _scrobbler {scrobbler} , _scrobbler{ scrobbler }
, _user {getDboPtr(user)} , _user{ getDboPtr(user) }
, _track {getDboPtr(track)} , _track{ getDboPtr(track) }
{} {}
Listen::pointer Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, 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, scrobbler, dateTime}}); }
}
std::size_t std::size_t Listen::getCount(Session& session)
Listen::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen"); }
}
Listen::pointer Listen::pointer Listen::find(Session& session, ListenId id)
Listen::find(Session& session, ListenId id) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue(); }
}
RangeResults<ListenId> RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
Listen::find(Session& session, const FindParameters& parameters) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<ListenId>("SELECT id FROM listen") auto query{ session.getDboSession().query<ListenId>("SELECT id FROM listen")
.orderBy("date_time")}; .orderBy("date_time") };
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.scrobbler)
query.where("scrobbler = ?").bind(*parameters.scrobbler); query.where("scrobbler = ?").bind(*parameters.scrobbler);
if (parameters.scrobblingState) if (parameters.scrobblingState)
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState); query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
return Utils::execQuery(query, parameters.range); return Utils::execQuery(query, parameters.range);
} }
Listen::pointer Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, 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("scrobbler = ?").bind(scrobbler)
.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> RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
Listen::getTopArtists(Session& session, {
UserId userId, auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
std::optional<TrackArtistLinkType> linkType,
Range range)
{
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)};
auto collection {query auto collection{ query
.orderBy("COUNT(a.id) DESC") .orderBy("COUNT(a.id) DESC")
.groupBy("a.id")}; .groupBy("a.id") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
Listen::getTopReleases(Session& session, {
UserId userId, auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
Scrobbler scrobbler, .orderBy("COUNT(r.id) DESC")
const std::vector<ClusterId>& clusterIds, .groupBy("r.id") };
Range range)
{
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(r.id) DESC")
.groupBy("r.id")};
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
Listen::getTopTracks(Session& session, {
UserId userId, auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
Scrobbler scrobbler, .orderBy("COUNT(t.id) DESC")
const std::vector<ClusterId>& clusterIds, .groupBy("t.id") };
Range range)
{
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.orderBy("COUNT(t.id) DESC")
.groupBy("t.id")};
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ArtistId> RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
Listen::getRecentArtists(Session& session, {
UserId userId, auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
Scrobbler scrobbler, .groupBy("a.id").having("l.date_time = MAX(l.date_time)")
const std::vector<ClusterId>& clusterIds, .orderBy("l.date_time DESC") };
std::optional<TrackArtistLinkType> linkType,
Range range)
{
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
Listen::getRecentReleases(Session& session, {
UserId userId, auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
Scrobbler scrobbler, .groupBy("r.id").having("l.date_time = MAX(l.date_time)")
const std::vector<ClusterId>& clusterIds, .orderBy("l.date_time DESC") };
Range range)
{
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<TrackId> RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range)
Listen::getRecentTracks(Session& session, {
UserId userId, auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
Scrobbler scrobbler, .groupBy("t.id").having("l.date_time = MAX(l.date_time)")
const std::vector<ClusterId>& clusterIds, .orderBy("l.date_time DESC") };
Range range)
{
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC")};
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId)
{
// TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON l.track_id = t.id")
.where("t.release_id = ?").bind(releaseId)
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)
.orderBy("l.date_time DESC")
.limit(1)
.resultValue();
}
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId trackId)
{
// TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON track_id = t.id")
.where("t.id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId)
.where("l.scrobbler = ?").bind(scrobbler)
.orderBy("l.date_time DESC")
.limit(1)
.resultValue();
}
} // namespace Database } // namespace Database
@@ -34,99 +34,76 @@
namespace Database namespace Database
{ {
class Session; class Session;
class Track; class Track;
class User; class User;
class Listen final : public Object<Listen, ListenId> class Listen final : public Object<Listen, ListenId>
{ {
public: public:
Listen() = default; Listen() = default;
struct FindParameters struct FindParameters
{ {
UserId user; UserId user;
std::optional<Scrobbler> scrobbler; std::optional<Scrobbler> scrobbler;
std::optional<ScrobblingState> scrobblingState; std::optional<ScrobblingState> scrobblingState;
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& setScrobbler(Scrobbler _scrobbler) { scrobbler = _scrobbler; return *this; }
FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; return *this; } FindParameters& setScrobblingState(ScrobblingState _scrobblingState) { scrobblingState = _scrobblingState; 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, Scrobbler scrobbler, 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, static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
UserId userId, static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
Scrobbler scrobbler, static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
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<TrackId> getTopTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
static RangeResults<ArtistId> getRecentArtists(Session& session, static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {});
UserId userId, static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
Scrobbler scrobbler, static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, Range range = {});
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<TrackId> getRecentTracks(Session& session,
UserId userId,
Scrobbler scrobbler,
const std::vector<ClusterId>& clusterIds,
Range range = {});
ScrobblingState getScrobblingState() const { return _scrobblingState; } static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
ObjectPtr<User> getUser() const { return _user; } static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
void setScrobblingState(ScrobblingState state) { _scrobblingState = state; } ScrobblingState getScrobblingState() const { return _scrobblingState; }
ObjectPtr<User> getUser() const { return _user; }
ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
template<class Action> void setScrobblingState(ScrobblingState state) { _scrobblingState = state; }
void persist(Action& a)
{
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); template<class Action>
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); void persist(Action& a)
} {
Wt::Dbo::field(a, _dateTime, "date_time");
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _scrobblingState, "scrobbling_state");
private: Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
friend class Session; Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime); }
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
Wt::WDateTime _dateTime; private:
Scrobbler _scrobbler; friend class Session;
ScrobblingState _scrobblingState {ScrobblingState::PendingAdd}; Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
static pointer create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime);
Wt::Dbo::ptr<User> _user; Wt::WDateTime _dateTime;
Wt::Dbo::ptr<Track> _track; Scrobbler _scrobbler;
}; ScrobblingState _scrobblingState{ ScrobblingState::PendingAdd };
Wt::Dbo::ptr<User> _user;
Wt::Dbo::ptr<Track> _track;
};
} // namespace Database } // namespace Database
File diff suppressed because it is too large Load Diff
@@ -133,6 +133,32 @@ namespace Scrobbling
return res; return res;
} }
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, releaseId) };
return listen ? listen->getDateTime() : Wt::WDateTime{};
}
Wt::WDateTime ScrobblingService::getLastListenDateTime(Database::UserId userId, Database::TrackId trackId)
{
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
const Database::Listen::pointer listen{ Database::Listen::getMostRecentListen(session, userId, *scrobbler, trackId) };
return listen ? listen->getDateTime() : Wt::WDateTime{};
}
// Top // Top
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
{ {
@@ -52,6 +52,9 @@ namespace Scrobbling
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override; Database::Range range) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) override;
Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) override;
ArtistContainer getTopArtists(Database::UserId userId, ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType, std::optional<Database::TrackArtistLinkType> linkType,
@@ -69,6 +69,9 @@ namespace Scrobbling
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0; Database::Range range) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual Wt::WDateTime getLastListenDateTime(Database::UserId userId, Database::TrackId trackId) = 0;
// Top // Top
virtual ArtistContainer getTopArtists(Database::UserId userId, virtual ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
@@ -138,6 +138,11 @@ namespace API::Subsonic
if (!id3) if (!id3)
albumNode.setAttribute("mediaType", "album"); albumNode.setAttribute("mediaType", "album");
{
const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), release->getId()) };
albumNode.setAttribute("played", dateTime.isValid() ? StringUtils::toISO8601String(dateTime) : "");
}
{ {
std::optional<UUID> mbid{ release->getMBID() }; std::optional<UUID> mbid{ release->getMBID() };
albumNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : ""); albumNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
@@ -162,6 +162,11 @@ namespace API::Subsonic
// OpenSubsonic specific fields (must always be set) // OpenSubsonic specific fields (must always be set)
trackResponse.setAttribute("mediaType", "song"); trackResponse.setAttribute("mediaType", "song");
{
const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), track->getId()) };
trackResponse.setAttribute("played", dateTime.isValid() ? StringUtils::toISO8601String(dateTime) : "");
}
{ {
std::optional<UUID> mbid{ track->getRecordingMBID() }; std::optional<UUID> mbid{ track->getRecordingMBID() };
trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : ""); trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");