1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-01 20:22:43 +00:00

core: remote playback send commands and update state

This commit is contained in:
Chris Josten 2023-01-04 21:32:27 +01:00
parent 77cb5d5957
commit 7a7ddc7717
18 changed files with 371 additions and 91 deletions

View file

@ -26,6 +26,7 @@ namespace Jellyfin {
namespace DTO {
class UserItemDataDto;
class PlaystateRequest;
class SessionInfo;
}
/**
@ -44,6 +45,13 @@ signals:
*/
void itemUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData);
/**
* @brief The information about a session has been updated
* @param sessionId The id of the session
* @param sessionInfo The associated information
*/
void sessionInfoUpdated(const QString &sessionId, const DTO::SessionInfo &sessionInfo);
/**
* @brief The server has requested to display an message to the user
* @param header The header of the message.

View file

@ -105,7 +105,7 @@ private:
class ControllableJellyfinSession : public ControllableSession {
Q_OBJECT
public:
ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo> info, QObject *parent = nullptr);
ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo> info, ApiClient &apiClient, QObject *parent = nullptr);
QString id() const override;
QString name() const override;
QString appName() const override;
@ -114,6 +114,7 @@ public:
PlaybackManager *createPlaybackManager() const override;
private:
QSharedPointer<DTO::SessionInfo> m_data;
ApiClient &m_apiClient;
};
/**

View file

@ -84,12 +84,11 @@ class PlaybackManager : public QObject {
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(Jellyfin::Model::MediaStatusClass::Value mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(Jellyfin::Model::Playlist *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
public:
explicit PlaybackManager(QObject *parent = nullptr);
virtual ~PlaybackManager();
virtual void swap(PlaybackManager& other) = 0;
void swap(PlaybackManager& other);
ApiClient * apiClient() const;
void setApiClient(ApiClient *apiClient);
@ -129,6 +128,9 @@ public:
* @param index Index of the item to play
*/
virtual void playItemInList(const QList<QSharedPointer<Model::Item>> &items, int index) = 0;
static const qint64 MS_TICK_FACTOR = 10000;
protected:
void setItem(QSharedPointer<Item> item);
signals:
void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newPlaybackState);
@ -190,8 +192,6 @@ class LocalPlaybackManager : public PlaybackManager {
public:
explicit LocalPlaybackManager(QObject *parent = nullptr);
void swap(PlaybackManager& other) override;
Player *player() const;
QString sessionId() const;
DTO::PlayMethod playMethod() const;

View file

@ -72,6 +72,11 @@ public:
*/
void next();
/**
* @brief Returns all items in the queue
*/
QList<QSharedPointer<Item>> queueAndList() const;
int queueSize() { return m_queue.size(); };
int listSize() const { return m_list.size(); };
int totalSize() const { return m_queue.size() + m_list.size(); }

View file

@ -20,10 +20,17 @@
#define JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H
#include <JellyfinQt/dto/generalcommandtype.h>
#include <JellyfinQt/dto/playcommand.h>
#include <JellyfinQt/dto/playstatecommand.h>
#include <JellyfinQt/dto/sessioninfo.h>
#include <JellyfinQt/model/playbackmanager.h>
#include <JellyfinQt/support/loader.h>
#include <QJsonObject>
#include <QSharedPointer>
#include <QTimer>
#include <optional>
namespace Jellyfin {
@ -33,11 +40,10 @@ namespace Model {
class RemoteJellyfinPlayback : public PlaybackManager {
public:
RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent = nullptr);
RemoteJellyfinPlayback(ApiClient &apiClient, QString sessionId, QObject *parent = nullptr);
virtual ~RemoteJellyfinPlayback();
// PlaybackManager
void swap(PlaybackManager &other) override;
PlayerState playbackState() const override;
MediaStatus mediaStatus() const override;
bool hasNext() const override;
@ -61,9 +67,19 @@ public slots:
void goTo(int index) override;
void stop() override;
void seek(qint64 pos) override;
private slots:
void onPositionTimerFired();
void onSessionInfoUpdated(const QString &sessionId, const DTO::SessionInfo &sessionInfo);
private:
void sendPlaystateCommand(DTO::PlaystateCommand command, qint64 seekTicks = -1);
void sendGeneralCommand(DTO::GeneralCommandType command, QJsonObject arguments = QJsonObject());
void sendCommand(Support::LoaderBase *loader);
void playItemInList(const QStringList &items, int index, qint64 resumeTicks = -1);
ApiClient &m_apiClient;
QString m_sessionId;
std::optional<DTO::SessionInfo> m_lastSessionInfo;
QTimer *m_positionTimer;
qint64 m_position = 0;
};

View file

@ -155,6 +155,8 @@ QString toString(const T &source, convertType<T>) {
return QJsonDocument(val.toArray()).toJson(format);
case QJsonValue::Object:
return QJsonDocument(val.toObject()).toJson(format);
case QJsonValue::String:
return val.toString();
case QJsonValue::Null:
default:
return QString();

View file

@ -334,9 +334,6 @@ private:
int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
m_reply->deleteLater();
m_reply = nullptr;
/*m_parsedWatcher.setFuture(QtConcurrent::run([this, statusCode, array]() {
return this->parseResponse(statusCode, array);
}));*/
m_parsedWatcher.setFuture(
QtConcurrent::run<typename HttpLoader<R, P>::ResultType, // Result
HttpLoader<R, P>, // class

View file

@ -71,6 +71,7 @@ public:
QVariant data(const QModelIndex &parent, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent) const override;
QHash<int, QByteArray> roleNames() const override;
void setPlaylistModel(Model::Playlist *data);
private slots:

View file

@ -60,9 +60,30 @@ public:
*/
explicit WebSocket(ApiClient *client);
enum MessageType {
/**
* @brief Server to client: instruct client to send periodical KeepAlive messages
*/
ForceKeepAlive,
/**
* @brief Client to server: keep the connection alive
*/
KeepAlive,
UserDataChanged
/**
* @brief Server to client: user data for an item has changed.
*/
UserDataChanged,
/**
* @brief Client to server: Subscribe to playback update sessions.
*/
SessionsStart,
/**
* @brief Client to server: unsubscribe from playback session updates
*/
SessionsStop,
/**
* @brief Server to client: session information has changed
*/
Sessions
};
Q_PROPERTY(QAbstractSocket::SocketState state READ state NOTIFY stateChanged)
Q_ENUM(MessageType)
@ -72,6 +93,8 @@ public:
}
public slots:
void open();
void subscribeToSessionInfo();
void unsubscribeToSessionInfo();
private slots:
void textMessageReceived(const QString &message);
void onConnected();
@ -80,7 +103,7 @@ private slots:
void sendKeepAlive();
void onWebsocketStateChanged(QAbstractSocket::SocketState newState) { emit stateChanged(newState); }
signals:
void commandReceived(QString arts, QVariantMap args);
void commandReceived(QString command, QVariantMap args);
void stateChanged(QAbstractSocket::SocketState newState);
protected:
@ -90,6 +113,7 @@ protected:
QTimer m_keepAliveTimer;
QTimer m_retryTimer;
int m_reconnectAttempt = 0;
int m_sessionInfoSubscribeCount = 0;
void setupKeepAlive(int data);

View file

@ -5,6 +5,7 @@
#include "JellyfinQt/loader/http/session.h"
#include "JellyfinQt/loader/requesttypes.h"
#include <JellyfinQt/model/playbackmanager.h>
#include <JellyfinQt/model/remotejellyfinplayback.h>
namespace Jellyfin {
@ -39,13 +40,16 @@ QString LocalSession::userName() const {
}
PlaybackManager *LocalSession::createPlaybackManager() const {
return new LocalPlaybackManager();
LocalPlaybackManager *playbackManager = new LocalPlaybackManager();
playbackManager->setApiClient(&m_apiClient);
return playbackManager;
}
// ControllableJellyfinSession
ControllableJellyfinSession::ControllableJellyfinSession(const QSharedPointer<DTO::SessionInfo> info, QObject *parent)
ControllableJellyfinSession::ControllableJellyfinSession(const QSharedPointer<DTO::SessionInfo> info, ApiClient &apiClient, QObject *parent)
: ControllableSession(parent),
m_data(info) {}
m_data(info),
m_apiClient(apiClient){}
QString ControllableJellyfinSession::id() const {
return m_data->jellyfinId();
@ -68,8 +72,7 @@ QString ControllableJellyfinSession::userName() const {
}
PlaybackManager * ControllableJellyfinSession::createPlaybackManager() const {
// TODO: implement
return nullptr;
return new RemoteJellyfinPlayback(m_apiClient, m_data->jellyfinId());
}
RemoteSessionScanner::RemoteSessionScanner(QObject *parent)
@ -114,7 +117,7 @@ void RemoteJellyfinSessionScanner::startScanning() {
// Skip this device
if (it->jellyfinId() == d->apiClient->deviceId()) continue;
emit sessionFound(new ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo>::create(*it)));
emit sessionFound(new ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo>::create(*it), *d->apiClient));
}
});
d->loader->load();

View file

@ -41,8 +41,8 @@ Item::Item(ApiClient *apiClient, QObject *parent)
}
Item::Item(const DTO::BaseItemDto &data, ApiClient *apiClient, QObject *parent)
: DTO::BaseItemDto(data),
QObject(parent),
: QObject(parent),
DTO::BaseItemDto(data),
m_apiClient(nullptr) {
setApiClient(apiClient);
}

View file

@ -59,7 +59,7 @@ public:
PlayerState m_state;
Model::Playlist *m_queue = nullptr;
Model::Playlist *m_queue;
int m_queueIndex = 0;
bool m_resumePlayback = false;
@ -153,6 +153,15 @@ int PlaybackManager::queueIndex() const {
return d->m_queueIndex;
}
void PlaybackManager::swap(PlaybackManager &other) {
other.queue()->clearList();
other.queue()->appendToList(this->queue()->queueAndList());
other.playItemInList(this->queue()->queueAndList(), this->queue()->currentItemIndexInList() >= 0
? this->queue()->currentItemIndexInList()
: 0);
other.seek(position());
}
void PlaybackManager::playItemId(const QString &id) {}
bool PlaybackManager::resumePlayback() const {
@ -188,6 +197,12 @@ void PlaybackManager::setSubtitleIndex(int newSubtitleIndex) {
emit subtitleIndexChanged(newSubtitleIndex);
}
void PlaybackManager::setItem(QSharedPointer<Item> item) {
Q_D(PlaybackManager);
d->m_item = item;
emit itemChanged();
}
/*****************************************************************************
* LocalPlaybackManagerPrivate *
*****************************************************************************/
@ -491,10 +506,6 @@ LocalPlaybackManager::LocalPlaybackManager(QObject *parent)
});
}
void LocalPlaybackManager::swap(PlaybackManager &other) {
Q_UNIMPLEMENTED();
}
Player* LocalPlaybackManager::player() const {
const Q_D(LocalPlaybackManager);
return d->m_mediaPlayer;

View file

@ -104,6 +104,14 @@ void Playlist::next() {
emit currentItemChanged();
}
QList<QSharedPointer<Item>> Playlist::queueAndList() const {
QList<QSharedPointer<Item>> result;
result.reserve(totalSize());
result.append(m_queue.toList());
result.append(m_list.toList());
return result;
}
QSharedPointer<const Item> Playlist::listAt(int index) const {
if (m_shuffler->canShuffleInAdvance()) {
return m_list.at(m_shuffler->itemAt(index));

View file

@ -20,90 +20,127 @@
#include <JellyfinQt/model/remotejellyfinplayback.h>
#include <JellyfinQt/apiclient.h>
#include <JellyfinQt/dto/sessioninfo.h>
#include <JellyfinQt/loader/http/session.h>
#include <JellyfinQt/loader/requesttypes.h>
#include <JellyfinQt/model/item.h>
#include <JellyfinQt/model/playlist.h>
#include <JellyfinQt/eventbus.h>
#include <JellyfinQt/websocket.h>
#include <QDebug>
namespace Jellyfin {
namespace Model {
RemoteJellyfinPlayback::RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent)
: PlaybackManager(parent), m_apiClient(apiClient) {
RemoteJellyfinPlayback::RemoteJellyfinPlayback(ApiClient &apiClient, QString sessionId, QObject *parent)
: PlaybackManager(parent), m_apiClient(apiClient), m_sessionId(sessionId), m_positionTimer(new QTimer(this)) {
setApiClient(&m_apiClient);
m_apiClient.websocket()->subscribeToSessionInfo();
connect(m_apiClient.eventbus(), &EventBus::sessionInfoUpdated, this, &RemoteJellyfinPlayback::onSessionInfoUpdated);
// Arm the timer
m_positionTimer->setInterval(1000);
connect(m_positionTimer, &QTimer::timeout, this, &RemoteJellyfinPlayback::onPositionTimerFired);
}
void RemoteJellyfinPlayback::swap(PlaybackManager &other) {
RemoteJellyfinPlayback::~RemoteJellyfinPlayback() {
m_apiClient.websocket()->unsubscribeToSessionInfo();
}
PlayerState RemoteJellyfinPlayback::playbackState() const {
return m_lastSessionInfo.has_value()
? m_lastSessionInfo.value().playState()->isPaused()
? PlayerState::Paused
: PlayerState::Playing
: PlayerState::Stopped;
}
MediaStatus RemoteJellyfinPlayback::mediaStatus() const {
return MediaStatus::Loaded;
}
bool RemoteJellyfinPlayback::hasNext() const {
return true;
}
bool RemoteJellyfinPlayback::hasPrevious() const {
return true;
}
PlaybackManagerError RemoteJellyfinPlayback::error() const {
return PlaybackManagerError::NoError;
}
const QString &RemoteJellyfinPlayback::errorString() const {
return m_sessionId;
}
qint64 RemoteJellyfinPlayback::position() const {
return m_position;
}
qint64 RemoteJellyfinPlayback::duration() const {
if (!m_lastSessionInfo.has_value()
|| m_lastSessionInfo.value().nowPlayingItem().isNull()) {
return 0;
}
return m_lastSessionInfo.value().nowPlayingItem()->runTimeTicks().value_or(0) / 10000;
}
bool RemoteJellyfinPlayback::seekable() const {
if (!m_lastSessionInfo.has_value()
|| m_lastSessionInfo.value().playState().isNull()) {
return false;
}
return m_lastSessionInfo.value().playState()->canSeek();
}
bool RemoteJellyfinPlayback::hasAudio() const {
return false;
}
bool RemoteJellyfinPlayback::hasVideo() const {
return false;
}
void RemoteJellyfinPlayback::playItem(QSharedPointer<Item> item) {
return playItemInList({item}, 0);
}
void RemoteJellyfinPlayback::playItemInList(const QList<QSharedPointer<Item> > &items, int index) {
// Map items to their ID
QStringList itemIds;
itemIds.reserve(items.size());
for(auto it = items.begin(); it < items.end(); it++) {
itemIds.append((*it)->jellyfinId());
}
if (this->resumePlayback()) {
this->playItemInList(itemIds, index, items.at(index)->userData()->playbackPositionTicks());
} else {
this->playItemInList(itemIds, index);
}
}
void RemoteJellyfinPlayback::pause() {
sendPlaystateCommand(DTO::PlaystateCommand::Pause);
}
void RemoteJellyfinPlayback::play() {
sendPlaystateCommand(DTO::PlaystateCommand::Unpause);
}
void RemoteJellyfinPlayback::playItemId(const QString &id) {
playItemInList({id}, 0);
}
void RemoteJellyfinPlayback::previous() {
sendPlaystateCommand(DTO::PlaystateCommand::PreviousTrack);
}
void RemoteJellyfinPlayback::next() {
sendPlaystateCommand(DTO::PlaystateCommand::NextTrack);
}
void RemoteJellyfinPlayback::goTo(int index) {
@ -111,21 +148,107 @@ void RemoteJellyfinPlayback::goTo(int index) {
}
void RemoteJellyfinPlayback::stop() {
sendPlaystateCommand(DTO::PlaystateCommand::Stop);
}
void RemoteJellyfinPlayback::seek(qint64 pos) {
sendPlaystateCommand(DTO::PlaystateCommand::Seek, pos * PlaybackManager::MS_TICK_FACTOR);
}
void RemoteJellyfinPlayback::onPositionTimerFired() {
m_position += m_positionTimer->interval();
emit positionChanged(position());
}
void RemoteJellyfinPlayback::onSessionInfoUpdated(const QString &sessionId, const SessionInfo &sessionInfo) {
if (sessionId != m_sessionId) return;
qDebug() << "Session info updated for " << sessionId;
m_lastSessionInfo = sessionInfo;
if (m_lastSessionInfo->nowPlayingItem().isNull()) {
setItem(QSharedPointer<Model::Item>::create());
} else {
Jellyfin::BaseItemDto itemData = *m_lastSessionInfo->nowPlayingItem().data();
setItem(QSharedPointer<Model::Item>::create(itemData, &m_apiClient));
}
// Update current position and run timer if needed
if (m_lastSessionInfo.has_value()
&& !m_lastSessionInfo.value().playState().isNull()) {
m_position = m_lastSessionInfo.value().playState()->positionTicks().value_or(0) / PlaybackManager::MS_TICK_FACTOR;
if (!m_positionTimer->isActive() && !m_lastSessionInfo.value().playState()->isPaused()) {
m_positionTimer->start();
} else if (m_positionTimer->isActive() && m_lastSessionInfo.value().playState()->isPaused()) {
m_positionTimer->stop();
}
} else if (m_positionTimer->isActive()){
m_positionTimer->stop();
m_position = 0;
}
emit playbackStateChanged(playbackState());
emit durationChanged(duration());
emit positionChanged(position());
}
void RemoteJellyfinPlayback::sendPlaystateCommand(DTO::PlaystateCommand command, qint64 seekTicks) {
using Params = Loader::SendPlaystateCommandParams;
using CommandLoader = Loader::HTTP::SendPlaystateCommandLoader;
Params params;
params.setCommand(command);
params.setSessionId(m_sessionId);
if (seekTicks >= 0) {
params.setSeekPositionTicks(seekTicks);
}
auto loader = new CommandLoader(&m_apiClient);
loader->setParameters(params);
sendCommand(loader);
}
void RemoteJellyfinPlayback::sendGeneralCommand(DTO::GeneralCommandType command, QJsonObject arguments) {
Loader::SendFullGeneralCommandParams params;
using Params = Loader::SendFullGeneralCommandParams;
using CommandLoader = Loader::HTTP::SendFullGeneralCommandLoader;
Params params;
QSharedPointer<DTO::GeneralCommand> fullCommand = QSharedPointer<DTO::GeneralCommand>::create(command, m_apiClient.userId());
fullCommand->setArguments(arguments);
// FIXME: send command
params.setBody(fullCommand);
params.setSessionId(m_sessionId);
auto loader = new CommandLoader(&m_apiClient);
loader->setParameters(params);
sendCommand(loader);
}
void RemoteJellyfinPlayback::sendCommand(Support::LoaderBase *loader) {
connect(loader, &Support::LoaderBase::ready, this, [loader](){
loader->deleteLater();
});
connect(loader, &Support::LoaderBase::error, this, [loader](QString message){
loader->deleteLater();
});
loader->load();
}
void RemoteJellyfinPlayback::playItemInList(const QStringList &items, int index, qint64 resumeTicks) {
using Params = Loader::PlayParams;
using CommandLoader = Loader::HTTP::PlayLoader;
Params params;
params.setSessionId(m_sessionId);
if (resumeTicks >= 0) {
params.setStartPositionTicks(resumeTicks);
}
params.setPlayCommand(DTO::PlayCommand::PlayNow);
params.setItemIds(items);
//params.setStartIndex(index);
CommandLoader *loader = new CommandLoader(&m_apiClient);
loader->setParameters(params);
sendCommand(loader);
}
} // NS Model
} // NS Jellyfin

View file

@ -46,37 +46,37 @@ PlayerAdaptor::~PlayerAdaptor() {
bool PlayerAdaptor::canControl() const
{
// get the value of property CanControl
return true;
return m_mediaControl->playbackManager() != nullptr;
}
bool PlayerAdaptor::canGoNext() const
{
// get the value of property CanGoNext
return canPlay() && m_mediaControl->playbackManager()->hasNext();
return canControl() && canPlay() && m_mediaControl->playbackManager()->hasNext();
}
bool PlayerAdaptor::canGoPrevious() const
{
// get the value of property CanGoPrevious
return canPlay() && m_mediaControl->playbackManager()->hasPrevious();
return canControl() && canPlay() && m_mediaControl->playbackManager()->hasPrevious();
}
bool PlayerAdaptor::canPause() const
{
// get the value of property CanPause
return canPlay();
return canControl() && canPlay();
}
bool PlayerAdaptor::canPlay() const
{
// get the value of property CanPlay
return m_mediaControl->playbackManager()->queue()->rowCount(QModelIndex()) > 0;
return canControl() && m_mediaControl->playbackManager()->queue()->rowCount(QModelIndex()) > 0;
}
bool PlayerAdaptor::canSeek() const
{
// get the value of property CanSeek
return m_mediaControl->playbackManager()->seekable();
return canControl() && m_mediaControl->playbackManager()->seekable();
}
QString PlayerAdaptor::loopStatus() const
@ -134,7 +134,10 @@ QVariantMap PlayerAdaptor::metadata() const
}
map[QStringLiteral("xesam:contentCreated")] = item->dateCreated();
map[QStringLiteral("xesam:genre")] = item->genres();
map[QStringLiteral("xesam:lastUsed")] = item->userData()->lastPlayedDate();
if (!item->userData().isNull()) {
map[QStringLiteral("xesam:lastUsed")] = item->userData()->lastPlayedDate();
}
QJsonObject providers = item->providerIds();
if (providers.contains(QStringLiteral("MusicBrainzTrack"))) {

View file

@ -75,25 +75,6 @@ PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent) {
QScopedPointer<PlaybackManagerPrivate> foo(new PlaybackManagerPrivate(this));
d_ptr.swap(foo);
Q_D(PlaybackManager);
// Set up connections.
connect(d->m_impl.data(), &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged);
if (auto localImp = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl.data())) {
connect(localImp, &Model::LocalPlaybackManager::streamUrlChanged, this, [this](const QUrl& newUrl){
emit this->streamUrlChanged(newUrl.toString());
});
connect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged);
}
connect(d->m_impl.data(), &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged);
}
PlaybackManager::~PlaybackManager() {
@ -175,12 +156,61 @@ void PlaybackManager::setControllingSession(QSharedPointer<Model::ControllableSe
qCDebug(playbackManager()) << "Now controlling session " << session->name();
session->setParent(this);
if (!d->m_impl.isNull()) {
disconnect(d->m_impl.data(), &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged);
disconnect(d->m_impl.data(), &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged);
if (auto localImp = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl.data())) {
disconnect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged);
}
disconnect(d->m_impl.data(), &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged);
}
Model::PlaybackManager *other = session->createPlaybackManager();
if (!d->m_impl.isNull()) {
bool thisIsLocal = qobject_cast<Model::LocalPlaybackManager *>(d->m_impl.data()) != nullptr;
//bool otherIsLocal = qobject_cast<Model::LocalPlaybackManager *>(other) != nullptr;
// Stop playing locally when switching to another session
if (thisIsLocal) {
d->m_impl->stop();
}
}
d->m_displayQueue->setPlaylistModel(other->queue());
d->m_impl.reset(other);
d->m_session.swap(session);
// TODO: swap out playback manager
emit controllingSessionChanged();
emit controllingSessionIdChanged();
emit controllingSessionNameChanged();
emit controllingSessionLocalChanged();
if (other != nullptr) {
connect(d->m_impl.data(), &Model::PlaybackManager::positionChanged, this, &PlaybackManager::positionChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::durationChanged, this, &PlaybackManager::durationChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::hasNextChanged, this, &PlaybackManager::hasNextChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::hasPreviousChanged, this, &PlaybackManager::hasPreviousChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::seekableChanged, this, &PlaybackManager::seekableChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::queueIndexChanged, this, &PlaybackManager::queueIndexChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::itemChanged, this, &PlaybackManager::mediaPlayerItemChanged);
connect(d->m_impl.data(), &Model::PlaybackManager::playbackStateChanged, this, &PlaybackManager::playbackStateChanged);
if (auto localImp = qobject_cast<Model::LocalPlaybackManager*>(d->m_impl.data())) {
connect(localImp, &Model::LocalPlaybackManager::streamUrlChanged, this, [this](const QUrl& newUrl){
emit this->streamUrlChanged(newUrl.toString());
});
connect(localImp, &Model::LocalPlaybackManager::playMethodChanged, this, &PlaybackManager::playMethodChanged);
}
connect(d->m_impl.data(), &Model::PlaybackManager::mediaStatusChanged, this, &PlaybackManager::mediaStatusChanged);
}
}
QString PlaybackManager::controllingSessionId() const {

View file

@ -26,20 +26,8 @@ namespace ViewModel {
Playlist::Playlist(Model::Playlist *data, QObject *parent)
: QAbstractListModel(parent),
m_data(data) {
connect(data, &Model::Playlist::beforeListCleared, this, &Playlist::onBeforePlaylistCleared);
connect(data, &Model::Playlist::listCleared, this, &Playlist::onPlaylistCleared);
connect(data, &Model::Playlist::beforeItemsAddedToList, this, &Playlist::onBeforeItemsAddedToList);
connect(data, &Model::Playlist::beforeItemsAddedToQueue, this, &Playlist::onBeforeItemsAddedToQueue);
connect(data, &Model::Playlist::itemsAddedToList, this, &Playlist::onItemsAddedToList);
connect(data, &Model::Playlist::itemsAddedToQueue, this, &Playlist::onItemsAddedToQueue);
connect(data, &Model::Playlist::beforeItemsRemovedFromList, this, &Playlist::onBeforeItemsRemovedFromList);
connect(data, &Model::Playlist::beforeItemsRemovedFromQueue, this, &Playlist::onBeforeItemsRemovedFromQueue);
connect(data, &Model::Playlist::itemsRemovedFromList, this, &Playlist::onItemsRemovedFromList);
connect(data, &Model::Playlist::itemsRemovedFromQueue, this, &Playlist::onItemsRemovedFromQueue);
connect(data, &Model::Playlist::listReshuffled, this, &Playlist::onReshuffled);
connect(data, &Model::Playlist::currentItemChanged, this, &Playlist::onPlayingItemChanged);
m_data(nullptr) {
setPlaylistModel(data);
}
int Playlist::rowCount(const QModelIndex &parent) const {
@ -55,6 +43,41 @@ QHash<int, QByteArray> Playlist::roleNames() const {
{RoleNames::section, "section"},
{RoleNames::playing, "playing"},
};
}
void Playlist::setPlaylistModel(Model::Playlist *data) {
if (m_data != nullptr) {
disconnect(data, &Model::Playlist::beforeListCleared, this, &Playlist::onBeforePlaylistCleared);
disconnect(data, &Model::Playlist::listCleared, this, &Playlist::onPlaylistCleared);
disconnect(data, &Model::Playlist::beforeItemsAddedToList, this, &Playlist::onBeforeItemsAddedToList);
disconnect(data, &Model::Playlist::beforeItemsAddedToQueue, this, &Playlist::onBeforeItemsAddedToQueue);
disconnect(data, &Model::Playlist::itemsAddedToList, this, &Playlist::onItemsAddedToList);
disconnect(data, &Model::Playlist::itemsAddedToQueue, this, &Playlist::onItemsAddedToQueue);
disconnect(data, &Model::Playlist::beforeItemsRemovedFromList, this, &Playlist::onBeforeItemsRemovedFromList);
disconnect(data, &Model::Playlist::beforeItemsRemovedFromQueue, this, &Playlist::onBeforeItemsRemovedFromQueue);
disconnect(data, &Model::Playlist::itemsRemovedFromList, this, &Playlist::onItemsRemovedFromList);
disconnect(data, &Model::Playlist::itemsRemovedFromQueue, this, &Playlist::onItemsRemovedFromQueue);
disconnect(data, &Model::Playlist::listReshuffled, this, &Playlist::onReshuffled);
disconnect(data, &Model::Playlist::currentItemChanged, this, &Playlist::onPlayingItemChanged);
}
beginResetModel();
m_data = data;
endResetModel();
if (m_data != nullptr) {
connect(data, &Model::Playlist::beforeListCleared, this, &Playlist::onBeforePlaylistCleared);
connect(data, &Model::Playlist::listCleared, this, &Playlist::onPlaylistCleared);
connect(data, &Model::Playlist::beforeItemsAddedToList, this, &Playlist::onBeforeItemsAddedToList);
connect(data, &Model::Playlist::beforeItemsAddedToQueue, this, &Playlist::onBeforeItemsAddedToQueue);
connect(data, &Model::Playlist::itemsAddedToList, this, &Playlist::onItemsAddedToList);
connect(data, &Model::Playlist::itemsAddedToQueue, this, &Playlist::onItemsAddedToQueue);
connect(data, &Model::Playlist::beforeItemsRemovedFromList, this, &Playlist::onBeforeItemsRemovedFromList);
connect(data, &Model::Playlist::beforeItemsRemovedFromQueue, this, &Playlist::onBeforeItemsRemovedFromQueue);
connect(data, &Model::Playlist::itemsRemovedFromList, this, &Playlist::onItemsRemovedFromList);
connect(data, &Model::Playlist::itemsRemovedFromQueue, this, &Playlist::onItemsRemovedFromQueue);
connect(data, &Model::Playlist::listReshuffled, this, &Playlist::onReshuffled);
connect(data, &Model::Playlist::currentItemChanged, this, &Playlist::onPlayingItemChanged);
}
};
QVariant Playlist::data(const QModelIndex &index, int role) const {

View file

@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <JellyfinQt/dto/generalcommand.h>
#include <JellyfinQt/dto/generalcommandtype.h>
#include <JellyfinQt/dto/playstaterequest.h>
#include <JellyfinQt/dto/sessioninfo.h>
#include <JellyfinQt/dto/useritemdatadto.h>
Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket");
@ -61,6 +62,21 @@ void WebSocket::open() {
qCDebug(jellyfinWebSocket) << "Opening WebSocket connection to " << m_webSocket.requestUrl() << ", connect attempt " << m_reconnectAttempt;
}
void WebSocket::subscribeToSessionInfo() {
if (m_sessionInfoSubscribeCount++ == 0) {
// First argument: initial delay in milliseconds
// Second argument: periodic update interval in milliseconds
// Reference: https://github.com/jellyfin/jellyfin/blob/f3c57e6a0ae015dc51cf548a0380d1bed33959c2/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs#L99
sendMessage(MessageType::SessionsStart, QJsonValue(QStringLiteral("0,5000")));
}
}
void WebSocket::unsubscribeToSessionInfo() {
if (--m_sessionInfoSubscribeCount == 0) {
sendMessage(MessageType::SessionsStop);
}
}
void WebSocket::onConnected() {
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
m_reconnectAttempt = 0;
@ -76,7 +92,6 @@ void WebSocket::onDisconnected() {
}
void WebSocket::textMessageReceived(const QString &message) {
qCDebug(jellyfinWebSocket) << "message received: " << message;
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isNull() || !doc.isObject()) {
qCWarning(jellyfinWebSocket()) << "Malformed message received over WebSocket: parse error or root not an object.";
@ -90,6 +105,7 @@ void WebSocket::textMessageReceived(const QString &message) {
// Convert the type so we can use it in our enums.
QString messageType = messageRoot["MessageType"].toString();
qCDebug(jellyfinWebSocket) << "Message received: " << messageType;
QJsonValue data = messageRoot["Data"];
if (messageType == QStringLiteral("ForceKeepAlive")) {
setupKeepAlive(data.toInt());
@ -136,8 +152,17 @@ void WebSocket::textMessageReceived(const QString &message) {
qCWarning(jellyfinWebSocket) << "Unparseable UserData list received: " << e->what();
}
}
} else if (messageType == QStringLiteral("Sessions")) {
try {
QList<DTO::SessionInfo> sessionInfoList = Support::fromJsonValue<QList<DTO::SessionInfo>>(data);
for (auto it = sessionInfoList.cbegin(); it != sessionInfoList.cend(); it++) {
emit m_apiClient->eventbus()->sessionInfoUpdated(it->jellyfinId(), *it);
}
} catch(QException *e) {
qCWarning(jellyfinWebSocket) << "Unparseable SessionInfo list received: " << e->what();
}
} else {
qCDebug(jellyfinWebSocket) << messageType;
qCDebug(jellyfinWebSocket) << "Unhandled message: " << messageType;
}
}