Implemented Subsonic command getNowPlaying, fixes #792
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user