diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 47d062a1..9fca99a5 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -180,7 +180,7 @@ namespace Database return session.getDboSession().query("SELECT COUNT(*) FROM artist"); } - std::vector Artist::find(Session& session, const std::string& name) + std::vector Artist::find(Session& session, std::string_view name) { session.checkReadTransaction(); diff --git a/src/libs/database/impl/Listen.cpp b/src/libs/database/impl/Listen.cpp index 57005412..87391546 100644 --- a/src/libs/database/impl/Listen.cpp +++ b/src/libs/database/impl/Listen.cpp @@ -25,112 +25,113 @@ #include "SqlQuery.hpp" #include "Utils.hpp" -namespace -{ - using namespace Database; - - Wt::Dbo::Query createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType) - { - auto query{ session.query("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 createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds) - { - auto query{ session.query("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 createTracksQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds) - { - auto query{ session.query("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 createArtistsQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType) + { + auto query{ session.query("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 createReleasesQuery(Wt::Dbo::Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds) + { + auto query{ session.query("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 createTracksQuery(Wt::Dbo::Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector& clusterIds) + { + auto query{ session.query("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, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) : _dateTime{ Wt::WDateTime::fromTime_t(dateTime.toTime_t()) } , _backend{ backend } @@ -212,7 +213,17 @@ namespace Database RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional 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(query, range); + } + + RangeResults Listen::getTopTracks(Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional 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 Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional 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 - diff --git a/src/libs/database/include/database/Artist.hpp b/src/libs/database/include/database/Artist.hpp index bbe03a64..24875565 100644 --- a/src/libs/database/include/database/Artist.hpp +++ b/src/libs/database/include/database/Artist.hpp @@ -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 find(Session& session, const std::string& name); // exact match on name field + static std::vector find(Session& session, std::string_view name); // exact match on name field static RangeResults find(Session& session, const FindParameters& parameters); static void find(Session& session, const FindParameters& parameters, std::function func); static RangeResults findIds(Session& session, const FindParameters& parameters); diff --git a/src/libs/database/include/database/Listen.hpp b/src/libs/database/include/database/Listen.hpp index 20e90b3b..b745112c 100644 --- a/src/libs/database/include/database/Listen.hpp +++ b/src/libs/database/include/database/Listen.hpp @@ -68,6 +68,7 @@ namespace Database static RangeResults getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); static RangeResults getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); static RangeResults getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); + static RangeResults getTopTracks(Session& session, UserId userId, ArtistId artistId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); static RangeResults getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, std::optional range = std::nullopt); static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional range = std::nullopt); diff --git a/src/libs/database/test/Listen.cpp b/src/libs/database/test/Listen.cpp index 6d075ced..e011b228 100644 --- a/src/libs/database/test/Listen.cpp +++ b/src/libs/database/test/Listen.cpp @@ -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" }; diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index 2770b045..eb5c52eb 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -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& 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 diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.hpp index 667897f7..c6f08508 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.hpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.hpp @@ -52,6 +52,7 @@ namespace Scrobbling ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::Range range) override; ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; TrackContainer getTopTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) override; + TrackContainer getTopTracks(Database::UserId userId, Database::ArtistId artistId, const std::vector& clusterIds, Database::Range range) override; std::optional getUserBackend(Database::UserId userId); diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index 892d9ee8..35ba2459 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -71,6 +71,7 @@ namespace Scrobbling virtual ArtistContainer getTopArtists(Database::UserId userId, const std::vector& clusterIds, std::optional linkType, Database::Range) = 0; virtual ReleaseContainer getTopReleases(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; virtual TrackContainer getTopTracks(Database::UserId userId, const std::vector& clusterIds, Database::Range range) = 0; + virtual TrackContainer getTopTracks(Database::UserId userId, Database::ArtistId artistId, const std::vector& clusterIds, Database::Range range) = 0; }; std::unique_ptr createScrobblingService(boost::asio::io_service& ioService, Database::Db& db); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index b472bf78..92797b30 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -182,7 +182,7 @@ namespace API::Subsonic {"/getAlbumInfo2", {handleNotImplemented}}, {"/getSimilarSongs", {handleGetSimilarSongsRequest}}, {"/getSimilarSongs2", {handleGetSimilarSongs2Request}}, - {"/getTopSongs", {handleNotImplemented}}, + {"/getTopSongs", {handleGetTopSongs}}, // Album/song lists {"/getAlbumList", {handleGetAlbumListRequest}}, diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 45765597..b6cbe1a4 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -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(context.parameters, "artist") }; + std::size_t count{ getParameterAs(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::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; + } } diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.hpp b/src/libs/subsonic/impl/entrypoints/Browsing.hpp index 3253db39..d87c787e 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.hpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.hpp @@ -36,4 +36,5 @@ namespace API::Subsonic Response handleGetArtistInfo2Request(RequestContext& context); Response handleGetSimilarSongsRequest(RequestContext& context); Response handleGetSimilarSongs2Request(RequestContext& context); + Response handleGetTopSongs(RequestContext& context); }