From 21119c72f61b739c499bacd59a7a87253f0a675b Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 12 Jan 2026 21:18:11 +0100 Subject: [PATCH] Implemented Subsonic command getNowPlaying, fixes #792 --- .../scrobbling/impl/ScrobblingService.cpp | 36 +++++++++++++++++++ .../scrobbling/impl/ScrobblingService.hpp | 19 ++++++++-- .../scrobbling/IScrobblingService.hpp | 11 ++++++ src/libs/subsonic/impl/SubsonicResource.cpp | 2 +- .../impl/endpoints/AlbumSongLists.cpp | 35 ++++++++++++++++++ .../impl/endpoints/AlbumSongLists.hpp | 1 + 6 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.cpp b/src/libs/services/scrobbling/impl/ScrobblingService.cpp index 2b9ae3eb..f40ac462 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.cpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.cpp @@ -77,6 +77,8 @@ namespace lms::scrobbling void ScrobblingService::listenStarted(const Listen& listen) { + insertNowPlayingEntry(listen); + if (std::optional backend{ getUserBackend(listen.userId) }) _scrobblingBackends[*backend]->listenStarted(listen); } @@ -93,6 +95,24 @@ namespace lms::scrobbling _scrobblingBackends[*backend]->addTimedListen(listen); } + void ScrobblingService::visitNowPlayingListens(const std::function& visitor, db::UserId userId) + { + const Clock::time_point now{ Clock::now() }; + + std::shared_lock lock{ _nowPlayingEntriesMutex }; + + for (const auto& [entryUserId, entry] : _nowPlayingEntries) + { + if (userId.isValid() && entryUserId != userId) + continue; + + if (entry.expiryAt <= now) + continue; + + visitor(entry.startedAt, Listen{ .userId = entryUserId, .trackId = entry.trackId }); + } + } + std::optional ScrobblingService::getUserBackend(UserId userId) { std::optional backend; @@ -253,4 +273,20 @@ namespace lms::scrobbling res = db::Listen::getTopTracks(session, listenFindParams); return res; } + + void ScrobblingService::insertNowPlayingEntry(const Listen& listen) + { + Session& session{ _db.getTLSSession() }; + auto transaction{ session.createReadTransaction() }; + + if (const db::Track::pointer track{ db::Track::find(session, listen.trackId) }) + { + const Clock::time_point now{ Clock::now() }; + + std::unique_lock lock{ _nowPlayingEntriesMutex }; + + // Add an extra delay to ensure the listen is not purged too early + _nowPlayingEntries.insert_or_assign(listen.userId, NowPlayingEntry{ .startedAt = now, .expiryAt = now + track->getDuration() + std::chrono::seconds{ 5 }, .trackId = listen.trackId }); + } + } } // namespace lms::scrobbling diff --git a/src/libs/services/scrobbling/impl/ScrobblingService.hpp b/src/libs/services/scrobbling/impl/ScrobblingService.hpp index 6fc26444..96ae5cfe 100644 --- a/src/libs/services/scrobbling/impl/ScrobblingService.hpp +++ b/src/libs/services/scrobbling/impl/ScrobblingService.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include "services/scrobbling/IScrobblingService.hpp" @@ -33,12 +34,16 @@ namespace lms::scrobbling { public: ScrobblingService(boost::asio::io_context& ioContext, db::IDb& db); - ~ScrobblingService(); + ~ScrobblingService() override; + + ScrobblingService(const ScrobblingService&) = delete; + ScrobblingService& operator=(const ScrobblingService&) = delete; private: void listenStarted(const Listen& listen) override; void listenFinished(const Listen& listen, std::optional duration) override; void addTimedListen(const TimedListen& listen) override; + void visitNowPlayingListens(const std::function& visitor, db::UserId userId) override; ArtistContainer getRecentArtists(const ArtistFindParameters& params) override; ReleaseContainer getRecentReleases(const FindParameters& params) override; @@ -56,8 +61,18 @@ namespace lms::scrobbling std::optional getUserBackend(db::UserId userId); + void insertNowPlayingEntry(const Listen& listen); + db::IDb& _db; std::unordered_map> _scrobblingBackends; - }; + std::shared_mutex _nowPlayingEntriesMutex; + struct NowPlayingEntry + { + Clock::time_point startedAt; + Clock::time_point expiryAt; + db::TrackId trackId; + }; + std::unordered_map _nowPlayingEntries; + }; } // namespace lms::scrobbling diff --git a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp index 9d5559a2..94a5f72b 100644 --- a/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp +++ b/src/libs/services/scrobbling/include/services/scrobbling/IScrobblingService.hpp @@ -32,6 +32,7 @@ #include "database/objects/ReleaseId.hpp" #include "database/objects/TrackId.hpp" #include "database/objects/Types.hpp" +#include "database/objects/UserId.hpp" #include "services/scrobbling/Listen.hpp" namespace lms::db @@ -46,12 +47,22 @@ namespace lms::scrobbling public: virtual ~IScrobblingService() = default; + using Clock = std::chrono::steady_clock; + // Scrobbling + + // Notify that a listen has started (for now-playing purposes) virtual void listenStarted(const Listen& listen) = 0; + + // Notify that a listen has finished (for scrobbling purposes) virtual void listenFinished(const Listen& listen, std::optional playedDuration = std::nullopt) = 0; + // Used to add listens afterwards (after some offline listening for example) virtual void addTimedListen(const TimedListen& listen) = 0; + // Visit all now-playing listens + virtual void visitNowPlayingListens(const std::function& visitor, db::UserId userId = {}) = 0; + // Stats using ArtistContainer = db::RangeResults; using ReleaseContainer = db::RangeResults; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 0210f6b4..7201d724 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -159,7 +159,7 @@ namespace lms::api::subsonic { "/getAlbumList2", { handleGetAlbumList2Request } }, { "/getRandomSongs", { handleGetRandomSongsRequest } }, { "/getSongsByGenre", { handleGetSongsByGenreRequest } }, - { "/getNowPlaying", { handleNotImplemented } }, + { "/getNowPlaying", { handleGetNowPlayingRequest } }, { "/getStarred", { handleGetStarredRequest } }, { "/getStarred2", { handleGetStarred2Request } }, diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp index 15e175a9..f79ef605 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.cpp @@ -296,6 +296,41 @@ namespace lms::api::subsonic return response; } + Response handleGetNowPlayingRequest(RequestContext& context) + { + Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; + Response::Node& nowPlayingNode{ response.createNode("nowPlaying") }; + + scrobbling::IScrobblingService& scrobblingService{ *core::Service::get() }; + + const auto now{ scrobbling::IScrobblingService::Clock::now() }; + + scrobblingService.visitNowPlayingListens([&](scrobbling::IScrobblingService::Clock::time_point startedAt, const scrobbling::Listen& listen) { + auto transaction{ context.getDbSession().createReadTransaction() }; + + // A regular user can only see his own now playing entry + if (!context.getUser()->isAdmin() && listen.userId != context.getUser()->getId()) + return; + + const User::pointer user{ User::find(context.getDbSession(), listen.userId) }; + if (!user) + return; + + const Track::pointer track{ Track::find(context.getDbSession(), listen.trackId) }; + if (!track) + return; + + auto NowPlayingEntryNode{ createSongNode(context, track, context.getUser()) }; + NowPlayingEntryNode.setAttribute("username", user->getLoginName()); + NowPlayingEntryNode.setAttribute("minutesAgo", static_cast(std::chrono::duration_cast(now - startedAt).count())); + NowPlayingEntryNode.setAttribute("playerId", user->getId().getValue()); // not sure what to put here + + nowPlayingNode.addArrayChild("song", std::move(NowPlayingEntryNode)); + }); + + return response; + } + Response handleGetStarredRequest(RequestContext& context) { return handleGetStarredRequestCommon(context, false /* no id3 */); diff --git a/src/libs/subsonic/impl/endpoints/AlbumSongLists.hpp b/src/libs/subsonic/impl/endpoints/AlbumSongLists.hpp index 63582d6f..c17d3479 100644 --- a/src/libs/subsonic/impl/endpoints/AlbumSongLists.hpp +++ b/src/libs/subsonic/impl/endpoints/AlbumSongLists.hpp @@ -28,6 +28,7 @@ namespace lms::api::subsonic Response handleGetAlbumList2Request(RequestContext& context); Response handleGetRandomSongsRequest(RequestContext& context); Response handleGetSongsByGenreRequest(RequestContext& context); + Response handleGetNowPlayingRequest(RequestContext& context); Response handleGetStarredRequest(RequestContext& context); Response handleGetStarred2Request(RequestContext& context); } // namespace lms::api::subsonic