OpenSubsonic API: added support for 'played' fields
This commit is contained in:
@@ -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`
|
||||||
|
|||||||
@@ -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() : "");
|
||||||
|
|||||||
Reference in New Issue
Block a user