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`
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ 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")
|
||||||
@@ -67,8 +66,7 @@ namespace
|
|||||||
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")
|
||||||
@@ -100,8 +98,7 @@ namespace
|
|||||||
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")
|
||||||
@@ -141,28 +138,24 @@ namespace Database
|
|||||||
, _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();
|
||||||
|
|
||||||
@@ -181,8 +174,7 @@ namespace Database
|
|||||||
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();
|
||||||
|
|
||||||
@@ -194,13 +186,7 @@ namespace Database
|
|||||||
.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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
std::optional<TrackArtistLinkType> linkType,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
|
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType) };
|
||||||
|
|
||||||
@@ -211,12 +197,7 @@ namespace Database
|
|||||||
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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.orderBy("COUNT(r.id) DESC")
|
.orderBy("COUNT(r.id) DESC")
|
||||||
@@ -225,12 +206,7 @@ namespace Database
|
|||||||
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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.orderBy("COUNT(t.id) DESC")
|
.orderBy("COUNT(t.id) DESC")
|
||||||
@@ -239,13 +215,7 @@ namespace Database
|
|||||||
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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
std::optional<TrackArtistLinkType> linkType,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
|
auto query{ createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
|
||||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -254,12 +224,7 @@ namespace Database
|
|||||||
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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -268,12 +233,7 @@ namespace Database
|
|||||||
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,
|
|
||||||
Scrobbler scrobbler,
|
|
||||||
const std::vector<ClusterId>& clusterIds,
|
|
||||||
Range range)
|
|
||||||
{
|
{
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||||
@@ -282,6 +242,30 @@ namespace Database
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -63,39 +63,16 @@ class Listen final : public Object<Listen, ListenId>
|
|||||||
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,
|
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, ReleaseId releaseId);
|
||||||
Range range = {});
|
static pointer getMostRecentListen(Session& session, UserId userId, Scrobbler scrobbler, TrackId releaseId);
|
||||||
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; }
|
ScrobblingState getScrobblingState() const { return _scrobblingState; }
|
||||||
ObjectPtr<User> getUser() const { return _user; }
|
ObjectPtr<User> getUser() const { return _user; }
|
||||||
|
|||||||
@@ -684,6 +684,59 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_FALSE(listen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
const auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, release.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||||
{
|
{
|
||||||
ScopedTrack track1{ session, "MyTrack1" };
|
ScopedTrack track1{ session, "MyTrack1" };
|
||||||
@@ -820,6 +873,52 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_FALSE(listen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime1 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime2{ Wt::WDate {1999, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime2 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wt::WDateTime dateTime3{ Wt::WDate {2001, 1, 2}, Wt::WTime {12,0, 1} };
|
||||||
|
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), Scrobbler::Internal, dateTime3 };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto listen{ Listen::getMostRecentListen(session, user->getId(), Scrobbler::Internal, track.getId()) };
|
||||||
|
EXPECT_TRUE(listen);
|
||||||
|
EXPECT_EQ(listen->getDateTime(), dateTime3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||||
{
|
{
|
||||||
ScopedTrack track1{ session, "MyTrack1" };
|
ScopedTrack track1{ session, "MyTrack1" };
|
||||||
|
|||||||
@@ -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