diff --git a/src/libs/services/database/impl/Listen.cpp b/src/libs/services/database/impl/Listen.cpp index c1462384..d7af57d0 100644 --- a/src/libs/services/database/impl/Listen.cpp +++ b/src/libs/services/database/impl/Listen.cpp @@ -140,6 +140,7 @@ namespace Database Listen::pointer Listen::create(Session& session, ObjectPtr user, ObjectPtr track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) { + session.checkUniqueLocked(); return session.getDboSession().add(std::unique_ptr {new Listen{ user, track, backend, dateTime }}); } @@ -188,6 +189,7 @@ namespace Database RangeResults Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) { + session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) }; auto collection{ query @@ -199,6 +201,7 @@ namespace Database RangeResults Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { + session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(r.id) DESC") .groupBy("r.id") }; @@ -208,6 +211,7 @@ namespace Database RangeResults Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { + session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .orderBy("COUNT(t.id) DESC") .groupBy("t.id") }; @@ -217,6 +221,7 @@ namespace Database RangeResults Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, std::optional linkType, Range range) { + session.checkSharedLocked(); auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) .groupBy("a.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; @@ -226,6 +231,7 @@ namespace Database RangeResults Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { + session.checkSharedLocked(); auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("r.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; @@ -235,6 +241,7 @@ namespace Database RangeResults Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range) { + session.checkSharedLocked(); auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) .groupBy("t.id").having("l.date_time = MAX(l.date_time)") .orderBy("l.date_time DESC") }; @@ -242,8 +249,38 @@ namespace Database return Utils::execQuery(query, range); } + std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) from listen l") + .where("l.track_id = ?").bind(trackId) + .where("l.user_id = ?").bind(userId) + .where("l.backend = ?").bind(backend) + .resultValue(); + } + + std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) + { + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT IFNULL(MIN(count_result), 0)" + " FROM (" + " SELECT COUNT(l.track_id) AS count_result" + " FROM track t" + " LEFT JOIN listen l ON t.id = l.track_id AND l.backend = ? AND l.user_id = ?" + " WHERE t.release_id = ?" + " GROUP BY t.id)") + .bind(backend) + .bind(userId) + .bind(releaseId) + .resultValue(); + } + Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) { + session.checkSharedLocked(); + // TODO not pending remove? return session.getDboSession().query>("SELECT l from listen l") .join("track t ON l.track_id = t.id") @@ -257,10 +294,10 @@ namespace Database Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) { + session.checkSharedLocked(); // TODO not pending remove? return session.getDboSession().query>("SELECT l from listen l") - .join("track t ON track_id = t.id") - .where("t.id = ?").bind(trackId) + .where("l.track_id = ?").bind(trackId) .where("l.user_id = ?").bind(userId) .where("l.backend = ?").bind(backend) .orderBy("l.date_time DESC") diff --git a/src/libs/services/database/impl/Session.cpp b/src/libs/services/database/impl/Session.cpp index 2e8307ae..13589cc1 100644 --- a/src/libs/services/database/impl/Session.cpp +++ b/src/libs/services/database/impl/Session.cpp @@ -160,6 +160,7 @@ namespace Database _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); diff --git a/src/libs/services/database/include/services/database/Listen.hpp b/src/libs/services/database/include/services/database/Listen.hpp index b61ac2ec..20930bc8 100644 --- a/src/libs/services/database/include/services/database/Listen.hpp +++ b/src/libs/services/database/include/services/database/Listen.hpp @@ -71,10 +71,13 @@ namespace Database static RangeResults getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); static RangeResults getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector& clusterIds, Range range = {}); + static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId); + static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId); + static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId); static pointer getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId releaseId); - SyncState getSyncState() const { return _syncState; } + SyncState getSyncState() const { return _syncState; } ObjectPtr getUser() const { return _user; } ObjectPtr getTrack() const { return _track; } const Wt::WDateTime& getDateTime() const { return _dateTime; } diff --git a/src/libs/services/database/test/Listen.cpp b/src/libs/services/database/test/Listen.cpp index 66da627d..04330574 100644 --- a/src/libs/services/database/test/Listen.cpp +++ b/src/libs/services/database/test/Listen.cpp @@ -291,7 +291,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases) const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; ScopedRelease release{ session, "MyRelease" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track.get().modify()->setRelease(release.get()); } @@ -333,7 +333,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi) ScopedRelease release2{ session, "MyRelease2" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track1.get().modify()->setRelease(release1.get()); track2.get().modify()->setRelease(release2.get()); } @@ -381,7 +381,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) ScopedRelease release{ session, "MyRelease" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track.get().modify()->setRelease(release.get()); } @@ -652,7 +652,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases) ScopedRelease release{ session, "MyRelease" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track.get().modify()->setRelease(release.get()); } @@ -692,7 +692,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease) ScopedRelease release{ session, "MyRelease" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track.get().modify()->setRelease(release.get()); } @@ -746,7 +746,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) ScopedRelease release2{ session, "MyRelease2" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track1.get().modify()->setRelease(release1.get()); track2.get().modify()->setRelease(release2.get()); } @@ -806,7 +806,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) ScopedRelease release{ session, "MyRelease" }; { - auto transaction{ session.createSharedTransaction() }; + auto transaction{ session.createUniqueTransaction() }; track.get().modify()->setRelease(release.get()); } { @@ -873,6 +873,71 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks) } } +TEST_F(DatabaseFixture, Listen_getCount_track) +{ + ScopedTrack track{ session, "MyTrack" }; + ScopedUser user{ session, "MyUser" }; + + { + auto transaction{ session.createSharedTransaction() }; + + const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; + EXPECT_EQ(count, 0); + } + + const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; + ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + + { + auto transaction{ session.createSharedTransaction() }; + + const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) }; + EXPECT_EQ(count, 1); + } +} + +TEST_F(DatabaseFixture, Listen_getCount_release) +{ + ScopedTrack track1{ session, "MyTrack" }; + ScopedTrack track2{ session, "MyTrack" }; + ScopedUser user{ session, "MyUser" }; + ScopedRelease release{ session, "MyRelease" }; + + auto getReleaseListenCount{ [&] + { + auto transaction{ session.createSharedTransaction() }; + return Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, release.getId()); + } }; + + EXPECT_EQ(getReleaseListenCount(), 0); + + const Wt::WDateTime dateTime1{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; + ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + + EXPECT_EQ(getReleaseListenCount(), 0); + + { + auto transaction{ session.createUniqueTransaction() }; + track1.get().modify()->setRelease(release.get()); + } + + EXPECT_EQ(getReleaseListenCount(), 1); + + { + auto transaction{ session.createUniqueTransaction() }; + track2.get().modify()->setRelease(release.get()); + } + + EXPECT_EQ(getReleaseListenCount(), 0); + ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + EXPECT_EQ(getReleaseListenCount(), 1); + ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + EXPECT_EQ(getReleaseListenCount(), 1); + + ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; + EXPECT_EQ(getReleaseListenCount(), 2); +} + TEST_F(DatabaseFixture, Listen_getMostRecentTrack) { ScopedTrack track{ session, "MyTrack" }; diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index 85e0ea94..cbee51d6 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -19,8 +19,9 @@ #include "responses/Album.hpp" -#include "services/database/Cluster.hpp" #include "services/database/Artist.hpp" +#include "services/database/Cluster.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/User.hpp" #include "services/feedback/IFeedbackService.hpp" @@ -124,6 +125,8 @@ namespace API::Subsonic } } + albumNode.setAttribute("playCount", Listen::getCount(context.dbSession, user->getId(), user->getScrobblingBackend(), release->getId())); + // Report the first GENRE for this track const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") }; if (genreClusterType) diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 7b71fa06..b2aab8d6 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -23,6 +23,7 @@ #include "services/database/Artist.hpp" #include "services/database/Cluster.hpp" +#include "services/database/Listen.hpp" #include "services/database/Release.hpp" #include "services/database/Track.hpp" #include "services/database/TrackArtistLink.hpp" @@ -106,7 +107,7 @@ namespace API::Subsonic trackResponse.setAttribute("discNumber", *track->getDiscNumber()); if (track->getYear()) trackResponse.setAttribute("year", *track->getYear()); - + trackResponse.setAttribute("playCount", Listen::getCount(context.dbSession, user->getId(), user->getScrobblingBackend(), track->getId())); trackResponse.setAttribute("path", getTrackPath(track)); { std::error_code ec;