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");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
||||
+117
-107
@@ -25,112 +25,113 @@
|
||||
#include "SqlQuery.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
|
||||
{
|
||||
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)
|
||||
: _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) }
|
||||
, _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)
|
||||
{
|
||||
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")
|
||||
.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)
|
||||
{
|
||||
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)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
|
||||
@@ -307,4 +318,3 @@ namespace Database
|
||||
.resultValue();
|
||||
}
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Database
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
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 void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||
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<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, 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<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)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
|
||||
@@ -214,5 +214,20 @@ namespace Scrobbling
|
||||
res = Database::Listen::getTopTracks(session, userId, *backend, clusterIds, range);
|
||||
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
|
||||
|
||||
|
||||
@@ -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;
|
||||
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, Database::ArtistId artistId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||
|
||||
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 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, 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);
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace API::Subsonic
|
||||
{"/getAlbumInfo2", {handleNotImplemented}},
|
||||
{"/getSimilarSongs", {handleGetSimilarSongsRequest}},
|
||||
{"/getSimilarSongs2", {handleGetSimilarSongs2Request}},
|
||||
{"/getTopSongs", {handleNotImplemented}},
|
||||
{"/getTopSongs", {handleGetTopSongs}},
|
||||
|
||||
// Album/song lists
|
||||
{"/getAlbumList", {handleGetAlbumListRequest}},
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/recommendation/IRecommendationService.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
@@ -480,4 +481,34 @@ namespace API::Subsonic
|
||||
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 handleGetSimilarSongsRequest(RequestContext& context);
|
||||
Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||
Response handleGetTopSongs(RequestContext& context);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user