Implemented Subsonic command getNowPlaying, fixes #792

This commit is contained in:
emeric
2026-01-12 21:18:11 +01:00
parent b08bcfe3da
commit 21119c72f6
6 changed files with 101 additions and 3 deletions
@@ -77,6 +77,8 @@ namespace lms::scrobbling
void ScrobblingService::listenStarted(const Listen& listen)
{
insertNowPlayingEntry(listen);
if (std::optional<ScrobblingBackend> 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<void(Clock::time_point startedAt, const Listen&)>& 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<ScrobblingBackend> ScrobblingService::getUserBackend(UserId userId)
{
std::optional<ScrobblingBackend> 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
@@ -21,6 +21,7 @@
#include <memory>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#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<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override;
void visitNowPlayingListens(const std::function<void(Clock::time_point startedAt, const Listen&)>& 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<db::ScrobblingBackend> getUserBackend(db::UserId userId);
void insertNowPlayingEntry(const Listen& listen);
db::IDb& _db;
std::unordered_map<db::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
};
std::shared_mutex _nowPlayingEntriesMutex;
struct NowPlayingEntry
{
Clock::time_point startedAt;
Clock::time_point expiryAt;
db::TrackId trackId;
};
std::unordered_map<db::UserId, NowPlayingEntry> _nowPlayingEntries;
};
} // namespace lms::scrobbling
@@ -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<std::chrono::seconds> 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<void(Clock::time_point startedAt, const Listen&)>& visitor, db::UserId userId = {}) = 0;
// Stats
using ArtistContainer = db::RangeResults<db::ArtistId>;
using ReleaseContainer = db::RangeResults<db::ReleaseId>;
+1 -1
View File
@@ -159,7 +159,7 @@ namespace lms::api::subsonic
{ "/getAlbumList2", { handleGetAlbumList2Request } },
{ "/getRandomSongs", { handleGetRandomSongsRequest } },
{ "/getSongsByGenre", { handleGetSongsByGenreRequest } },
{ "/getNowPlaying", { handleNotImplemented } },
{ "/getNowPlaying", { handleGetNowPlayingRequest } },
{ "/getStarred", { handleGetStarredRequest } },
{ "/getStarred2", { handleGetStarred2Request } },
@@ -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<scrobbling::IScrobblingService>::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<int>(std::chrono::duration_cast<std::chrono::minutes>(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 */);
@@ -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