Implemented Subsonic command getNowPlaying, fixes #792
This commit is contained in:
@@ -77,6 +77,8 @@ namespace lms::scrobbling
|
|||||||
|
|
||||||
void ScrobblingService::listenStarted(const Listen& listen)
|
void ScrobblingService::listenStarted(const Listen& listen)
|
||||||
{
|
{
|
||||||
|
insertNowPlayingEntry(listen);
|
||||||
|
|
||||||
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
if (std::optional<ScrobblingBackend> backend{ getUserBackend(listen.userId) })
|
||||||
_scrobblingBackends[*backend]->listenStarted(listen);
|
_scrobblingBackends[*backend]->listenStarted(listen);
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,24 @@ namespace lms::scrobbling
|
|||||||
_scrobblingBackends[*backend]->addTimedListen(listen);
|
_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> ScrobblingService::getUserBackend(UserId userId)
|
||||||
{
|
{
|
||||||
std::optional<ScrobblingBackend> backend;
|
std::optional<ScrobblingBackend> backend;
|
||||||
@@ -253,4 +273,20 @@ namespace lms::scrobbling
|
|||||||
res = db::Listen::getTopTracks(session, listenFindParams);
|
res = db::Listen::getTopTracks(session, listenFindParams);
|
||||||
return res;
|
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
|
} // namespace lms::scrobbling
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
@@ -33,12 +34,16 @@ namespace lms::scrobbling
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ScrobblingService(boost::asio::io_context& ioContext, db::IDb& db);
|
ScrobblingService(boost::asio::io_context& ioContext, db::IDb& db);
|
||||||
~ScrobblingService();
|
~ScrobblingService() override;
|
||||||
|
|
||||||
|
ScrobblingService(const ScrobblingService&) = delete;
|
||||||
|
ScrobblingService& operator=(const ScrobblingService&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void listenStarted(const Listen& listen) override;
|
void listenStarted(const Listen& listen) override;
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
void addTimedListen(const TimedListen& listen) 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;
|
ArtistContainer getRecentArtists(const ArtistFindParameters& params) override;
|
||||||
ReleaseContainer getRecentReleases(const FindParameters& params) override;
|
ReleaseContainer getRecentReleases(const FindParameters& params) override;
|
||||||
@@ -56,8 +61,18 @@ namespace lms::scrobbling
|
|||||||
|
|
||||||
std::optional<db::ScrobblingBackend> getUserBackend(db::UserId userId);
|
std::optional<db::ScrobblingBackend> getUserBackend(db::UserId userId);
|
||||||
|
|
||||||
|
void insertNowPlayingEntry(const Listen& listen);
|
||||||
|
|
||||||
db::IDb& _db;
|
db::IDb& _db;
|
||||||
std::unordered_map<db::ScrobblingBackend, std::unique_ptr<IScrobblingBackend>> _scrobblingBackends;
|
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
|
} // namespace lms::scrobbling
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "database/objects/ReleaseId.hpp"
|
#include "database/objects/ReleaseId.hpp"
|
||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
#include "database/objects/Types.hpp"
|
#include "database/objects/Types.hpp"
|
||||||
|
#include "database/objects/UserId.hpp"
|
||||||
#include "services/scrobbling/Listen.hpp"
|
#include "services/scrobbling/Listen.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -46,12 +47,22 @@ namespace lms::scrobbling
|
|||||||
public:
|
public:
|
||||||
virtual ~IScrobblingService() = default;
|
virtual ~IScrobblingService() = default;
|
||||||
|
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
// Scrobbling
|
// Scrobbling
|
||||||
|
|
||||||
|
// Notify that a listen has started (for now-playing purposes)
|
||||||
virtual void listenStarted(const Listen& listen) = 0;
|
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;
|
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;
|
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
|
// Stats
|
||||||
using ArtistContainer = db::RangeResults<db::ArtistId>;
|
using ArtistContainer = db::RangeResults<db::ArtistId>;
|
||||||
using ReleaseContainer = db::RangeResults<db::ReleaseId>;
|
using ReleaseContainer = db::RangeResults<db::ReleaseId>;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ namespace lms::api::subsonic
|
|||||||
{ "/getAlbumList2", { handleGetAlbumList2Request } },
|
{ "/getAlbumList2", { handleGetAlbumList2Request } },
|
||||||
{ "/getRandomSongs", { handleGetRandomSongsRequest } },
|
{ "/getRandomSongs", { handleGetRandomSongsRequest } },
|
||||||
{ "/getSongsByGenre", { handleGetSongsByGenreRequest } },
|
{ "/getSongsByGenre", { handleGetSongsByGenreRequest } },
|
||||||
{ "/getNowPlaying", { handleNotImplemented } },
|
{ "/getNowPlaying", { handleGetNowPlayingRequest } },
|
||||||
{ "/getStarred", { handleGetStarredRequest } },
|
{ "/getStarred", { handleGetStarredRequest } },
|
||||||
{ "/getStarred2", { handleGetStarred2Request } },
|
{ "/getStarred2", { handleGetStarred2Request } },
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,41 @@ namespace lms::api::subsonic
|
|||||||
return response;
|
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)
|
Response handleGetStarredRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
return handleGetStarredRequestCommon(context, false /* no id3 */);
|
return handleGetStarredRequestCommon(context, false /* no id3 */);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace lms::api::subsonic
|
|||||||
Response handleGetAlbumList2Request(RequestContext& context);
|
Response handleGetAlbumList2Request(RequestContext& context);
|
||||||
Response handleGetRandomSongsRequest(RequestContext& context);
|
Response handleGetRandomSongsRequest(RequestContext& context);
|
||||||
Response handleGetSongsByGenreRequest(RequestContext& context);
|
Response handleGetSongsByGenreRequest(RequestContext& context);
|
||||||
|
Response handleGetNowPlayingRequest(RequestContext& context);
|
||||||
Response handleGetStarredRequest(RequestContext& context);
|
Response handleGetStarredRequest(RequestContext& context);
|
||||||
Response handleGetStarred2Request(RequestContext& context);
|
Response handleGetStarred2Request(RequestContext& context);
|
||||||
} // namespace lms::api::subsonic
|
} // namespace lms::api::subsonic
|
||||||
|
|||||||
Reference in New Issue
Block a user