Subsonic API: added getTopSongs endpoint, fixes #365
This commit is contained in:
@@ -180,7 +180,7 @@ namespace Database
|
|||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
|
std::vector<Artist::pointer> Artist::find(Session& session, std::string_view name)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
|||||||
+117
-107
@@ -25,112 +25,113 @@
|
|||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
|
||||||
{
|
|
||||||
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
|
|
||||||
.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("listen l ON l.track_id = t.id")
|
|
||||||
.where("l.user_id = ?").bind(userId)
|
|
||||||
.where("l.backend = ?").bind(backend) };
|
|
||||||
|
|
||||||
if (linkType)
|
|
||||||
query.where("t_a_l.type = ?").bind(*linkType);
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
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_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 track_cluster t_c ON t_c.track_id = t.id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
|
||||||
{
|
|
||||||
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
|
||||||
.join("track t ON t.release_id = r.id")
|
|
||||||
.join("listen l ON l.track_id = t.id")
|
|
||||||
.where("l.user_id = ?").bind(userId)
|
|
||||||
.where("l.backend = ?").bind(backend) };
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
|
||||||
" INNER JOIN track t ON t.release_id = r.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";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (ClusterId id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
|
||||||
{
|
|
||||||
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
|
||||||
.join("listen l ON l.track_id = t.id")
|
|
||||||
.where("l.user_id = ?").bind(userId)
|
|
||||||
.where("l.backend = ?").bind(backend) };
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
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 cluster c ON c.id = t_c.cluster_id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||||
|
{
|
||||||
|
auto query{ session.query<ArtistId>("SELECT a.id from artist a")
|
||||||
|
.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("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
|
if (linkType)
|
||||||
|
query.where("t_a_l.type = ?").bind(*linkType);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
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_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 track_cluster t_c ON t_c.track_id = t.id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query{ session.query<ReleaseId>("SELECT r.id from release r")
|
||||||
|
.join("track t ON t.release_id = r.id")
|
||||||
|
.join("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
|
" INNER JOIN track t ON t.release_id = r.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";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (ClusterId id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds)
|
||||||
|
{
|
||||||
|
auto query{ session.query<TrackId>("SELECT t.id from track t")
|
||||||
|
.join("listen l ON l.track_id = t.id")
|
||||||
|
.where("l.user_id = ?").bind(userId)
|
||||||
|
.where("l.backend = ?").bind(backend) };
|
||||||
|
|
||||||
|
if (artistId.isValid())
|
||||||
|
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id").where("t_a_l.artist_id = ?").bind(artistId);
|
||||||
|
|
||||||
|
if (!clusterIds.empty())
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
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 cluster c ON c.id = t_c.cluster_id";
|
||||||
|
|
||||||
|
WhereClause clusterClause;
|
||||||
|
for (auto id : clusterIds)
|
||||||
|
{
|
||||||
|
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||||
|
query.bind(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||||
|
|
||||||
|
query.where(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||||
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
||||||
, _backend{ backend }
|
, _backend{ backend }
|
||||||
@@ -212,7 +213,17 @@ namespace Database
|
|||||||
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, ArtistId{}, backend, clusterIds)
|
||||||
|
.orderBy("COUNT(t.id) DESC")
|
||||||
|
.groupBy("t.id") };
|
||||||
|
|
||||||
|
return Utils::execQuery<TrackId>(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
auto query{ createTracksQuery(session.getDboSession(), userId, artistId, backend, clusterIds)
|
||||||
.orderBy("COUNT(t.id) DESC")
|
.orderBy("COUNT(t.id) DESC")
|
||||||
.groupBy("t.id") };
|
.groupBy("t.id") };
|
||||||
|
|
||||||
@@ -242,7 +253,7 @@ namespace Database
|
|||||||
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
auto query{ createTracksQuery(session.getDboSession(), userId, ArtistId{}, backend, clusterIds)
|
||||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||||
.orderBy("l.date_time DESC") };
|
.orderBy("l.date_time DESC") };
|
||||||
|
|
||||||
@@ -307,4 +318,3 @@ namespace Database
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace Database
|
|||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, const UUID& MBID);
|
static pointer find(Session& session, const UUID& MBID);
|
||||||
static pointer find(Session& session, ArtistId id);
|
static pointer find(Session& session, ArtistId id);
|
||||||
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
|
static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field
|
||||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||||
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ namespace Database
|
|||||||
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
||||||
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||||
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||||
|
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
|
||||||
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
|
||||||
|
|||||||
@@ -438,6 +438,47 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Listen_getTopTracks_artist)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
ScopedUser user{ session, "MyUser" };
|
||||||
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
|
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
auto tracks{ Listen::getTopTracks(session, user->getId(), artist->getId(), ScrobblingBackend::Internal, {}) };
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
auto tracks{ Listen::getTopTracks(session, user->getId(), artist->getId(), ScrobblingBackend::Internal, {}) };
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
auto tracks{ Listen::getTopTracks(session, user->getId(), artist->getId(), ScrobblingBackend::Internal, {}) };
|
||||||
|
EXPECT_EQ(tracks.moreResults, false);
|
||||||
|
ASSERT_EQ(tracks.results.size(), 1);
|
||||||
|
EXPECT_EQ(tracks.results[0], track.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||||
{
|
{
|
||||||
ScopedTrack track1{ session, "MyTrack1" };
|
ScopedTrack track1{ session, "MyTrack1" };
|
||||||
|
|||||||
@@ -214,5 +214,20 @@ namespace Scrobbling
|
|||||||
res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
|
res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrobblingService::TrackContainer ScrobblingService::getTopTracks(UserId userId, Database::ArtistId artistId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
|
{
|
||||||
|
TrackContainer res;
|
||||||
|
|
||||||
|
const auto backend{ getUserBackend(userId) };
|
||||||
|
if (!backend)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
res = Database::Listen::getTopTracks(session, userId, artistId, *backend, clusterIds, range);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ namespace Scrobbling
|
|||||||
ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) override;
|
ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range range) override;
|
||||||
ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
TrackContainer getTopTracks(Database::UserId userId, Database::ArtistId artistId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
std::optional<Database::ScrobblingBackend> getUserBackend(Database::UserId userId);
|
std::optional<Database::ScrobblingBackend> getUserBackend(Database::UserId userId);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ namespace Scrobbling
|
|||||||
virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range) = 0;
|
virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::Range) = 0;
|
||||||
virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
|
virtual TrackContainer getTopTracks(Database::UserId userId, Database::ArtistId artistId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ namespace API::Subsonic
|
|||||||
{"/getAlbumInfo2", {handleNotImplemented}},
|
{"/getAlbumInfo2", {handleNotImplemented}},
|
||||||
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
||||||
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
||||||
{"/getTopSongs", {handleNotImplemented}},
|
{"/getTopSongs", {handleGetTopSongs}},
|
||||||
|
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
{"/getAlbumList", {handleGetAlbumListRequest}},
|
{"/getAlbumList", {handleGetAlbumListRequest}},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
#include "utils/ILogger.hpp"
|
#include "utils/ILogger.hpp"
|
||||||
#include "utils/Random.hpp"
|
#include "utils/Random.hpp"
|
||||||
#include "utils/Service.hpp"
|
#include "utils/Service.hpp"
|
||||||
@@ -480,4 +481,34 @@ namespace API::Subsonic
|
|||||||
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
|
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response handleGetTopSongs(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
std::string_view artistName{ getMandatoryParameterAs<std::string_view>(context.parameters, "artist") };
|
||||||
|
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(50) };
|
||||||
|
if (count > defaultMaxCountSize)
|
||||||
|
throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto artists{ Artist::find(context.dbSession, artistName) };
|
||||||
|
if (artists.size() != 1)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& topSongs{ response.createNode("topSongs") };
|
||||||
|
|
||||||
|
const auto trackIds{ Service<Scrobbling::IScrobblingService>::get()->getTopTracks(context.userId, artists.front()->getId(), {}, Database::Range{ 0, count }) };
|
||||||
|
for (const TrackId trackId : trackIds.results)
|
||||||
|
{
|
||||||
|
if (Track::pointer track{ Track::find(context.dbSession, trackId) })
|
||||||
|
topSongs.addArrayChild("song", createSongNode(context, track, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ namespace API::Subsonic
|
|||||||
Response handleGetArtistInfo2Request(RequestContext& context);
|
Response handleGetArtistInfo2Request(RequestContext& context);
|
||||||
Response handleGetSimilarSongsRequest(RequestContext& context);
|
Response handleGetSimilarSongsRequest(RequestContext& context);
|
||||||
Response handleGetSimilarSongs2Request(RequestContext& context);
|
Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||||
|
Response handleGetTopSongs(RequestContext& context);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user