1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-11-22 09:15:18 +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 { namespace DTO {
class UserItemDataDto; class UserItemDataDto;
class PlaystateRequest; class PlaystateRequest;
class SessionInfo;
} }
/** /**
@ -44,6 +45,13 @@ signals:
*/ */
void itemUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData); 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 * @brief The server has requested to display an message to the user
* @param header The header of the message. * @param header The header of the message.

View file

@ -105,7 +105,7 @@ private:
class ControllableJellyfinSession : public ControllableSession { class ControllableJellyfinSession : public ControllableSession {
Q_OBJECT Q_OBJECT
public: public:
ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo> info, QObject *parent = nullptr); ControllableJellyfinSession(QSharedPointer<DTO::SessionInfo> info, ApiClient &apiClient, QObject *parent = nullptr);
QString id() const override; QString id() const override;
QString name() const override; QString name() const override;
QString appName() const override; QString appName() const override;
@ -114,6 +114,7 @@ public:
PlaybackManager *createPlaybackManager() const override; PlaybackManager *createPlaybackManager() const override;
private: private:
QSharedPointer<DTO::SessionInfo> m_data; 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(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(Jellyfin::Model::PlayerStateClass::Value playbackState READ playbackState NOTIFY playbackStateChanged) 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::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) Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
public: public:
explicit PlaybackManager(QObject *parent = nullptr); explicit PlaybackManager(QObject *parent = nullptr);
virtual ~PlaybackManager(); virtual ~PlaybackManager();
virtual void swap(PlaybackManager& other) = 0; void swap(PlaybackManager& other);
ApiClient * apiClient() const; ApiClient * apiClient() const;
void setApiClient(ApiClient *apiClient); void setApiClient(ApiClient *apiClient);
@ -129,6 +128,9 @@ public:
* @param index Index of the item to play * @param index Index of the item to play
*/ */
virtual void playItemInList(const QList<QSharedPointer<Model::Item>> &items, int index) = 0; 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: signals:
void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newPlaybackState); void playbackStateChanged(Jellyfin::Model::PlayerStateClass::Value newPlaybackState);
@ -190,8 +192,6 @@ class LocalPlaybackManager : public PlaybackManager {
public: public:
explicit LocalPlaybackManager(QObject *parent = nullptr); explicit LocalPlaybackManager(QObject *parent = nullptr);
void swap(PlaybackManager& other) override;
Player *player() const; Player *player() const;
QString sessionId() const; QString sessionId() const;
DTO::PlayMethod playMethod() const; DTO::PlayMethod playMethod() const;

View file

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

View file

@ -20,10 +20,17 @@
#define JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H #define JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H
#include <JellyfinQt/dto/generalcommandtype.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/model/playbackmanager.h>
#include <JellyfinQt/support/loader.h>
#include <QJsonObject> #include <QJsonObject>
#include <QSharedPointer> #include <QSharedPointer>
#include <QTimer>
#include <optional>
namespace Jellyfin { namespace Jellyfin {
@ -33,11 +40,10 @@ namespace Model {
class RemoteJellyfinPlayback : public PlaybackManager { class RemoteJellyfinPlayback : public PlaybackManager {
public: public:
RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent = nullptr); RemoteJellyfinPlayback(ApiClient &apiClient, QString sessionId, QObject *parent = nullptr);
virtual ~RemoteJellyfinPlayback();
// PlaybackManager // PlaybackManager
void swap(PlaybackManager &other) override;
PlayerState playbackState() const override; PlayerState playbackState() const override;
MediaStatus mediaStatus() const override; MediaStatus mediaStatus() const override;
bool hasNext() const override; bool hasNext() const override;
@ -61,9 +67,19 @@ public slots:
void goTo(int index) override; void goTo(int index) override;
void stop() override; void stop() override;
void seek(qint64 pos) override; void seek(qint64 pos) override;
private slots:
void onPositionTimerFired();
void onSessionInfoUpdated(const QString &sessionId, const DTO::SessionInfo &sessionInfo);
private: private:
void sendPlaystateCommand(DTO::PlaystateCommand command, qint64 seekTicks = -1);
void sendGeneralCommand(DTO::GeneralCommandType command, QJsonObject arguments = QJsonObject()); 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; 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); return QJsonDocument(val.toArray()).toJson(format);
case QJsonValue::Object: case QJsonValue::Object:
return QJsonDocument(val.toObject()).toJson(format); return QJsonDocument(val.toObject()).toJson(format);
case QJsonValue::String:
return val.toString();
case QJsonValue::Null: case QJsonValue::Null:
default: default:
return QString(); return QString();

View file

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

View file

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

View file

@ -60,9 +60,30 @@ public:
*/ */
explicit WebSocket(ApiClient *client); explicit WebSocket(ApiClient *client);
enum MessageType { enum MessageType {
/**
* @brief Server to client: instruct client to send periodical KeepAlive messages
*/
ForceKeepAlive, ForceKeepAlive,
/**
* @brief Client to server: keep the connection alive
*/
KeepAlive, 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_PROPERTY(QAbstractSocket::SocketState state READ state NOTIFY stateChanged)
Q_ENUM(MessageType) Q_ENUM(MessageType)
@ -72,6 +93,8 @@ public:
} }
public slots: public slots:
void open(); void open();
void subscribeToSessionInfo();
void unsubscribeToSessionInfo();
private slots: private slots:
void textMessageReceived(const QString &message); void textMessageReceived(const QString &message);
void onConnected(); void onConnected();
@ -80,7 +103,7 @@ private slots:
void sendKeepAlive(); void sendKeepAlive();
void onWebsocketStateChanged(QAbstractSocket::SocketState newState) { emit stateChanged(newState); } void onWebsocketStateChanged(QAbstractSocket::SocketState newState) { emit stateChanged(newState); }
signals: signals:
void commandReceived(QString arts, QVariantMap args); void commandReceived(QString command, QVariantMap args);
void stateChanged(QAbstractSocket::SocketState newState); void stateChanged(QAbstractSocket::SocketState newState);
protected: protected:
@ -90,6 +113,7 @@ protected:
QTimer m_keepAliveTimer; QTimer m_keepAliveTimer;
QTimer m_retryTimer; QTimer m_retryTimer;
int m_reconnectAttempt = 0; int m_reconnectAttempt = 0;
int m_sessionInfoSubscribeCount = 0;
void setupKeepAlive(int data); void setupKeepAlive(int data);

View file

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

View file

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

View file

@ -104,6 +104,14 @@ void Playlist::next() {
emit currentItemChanged(); 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 { QSharedPointer<const Item> Playlist::listAt(int index) const {
if (m_shuffler->canShuffleInAdvance()) { if (m_shuffler->canShuffleInAdvance()) {
return m_list.at(m_shuffler->itemAt(index)); return m_list.at(m_shuffler->itemAt(index));

View file

@ -20,90 +20,127 @@
#include <JellyfinQt/model/remotejellyfinplayback.h> #include <JellyfinQt/model/remotejellyfinplayback.h>
#include <JellyfinQt/apiclient.h> #include <JellyfinQt/apiclient.h>
#include <JellyfinQt/dto/sessioninfo.h>
#include <JellyfinQt/loader/http/session.h> #include <JellyfinQt/loader/http/session.h>
#include <JellyfinQt/loader/requesttypes.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 Jellyfin {
namespace Model { namespace Model {
RemoteJellyfinPlayback::RemoteJellyfinPlayback(ApiClient &apiClient, QObject *parent) RemoteJellyfinPlayback::RemoteJellyfinPlayback(ApiClient &apiClient, QString sessionId, QObject *parent)
: PlaybackManager(parent), m_apiClient(apiClient) { : 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 { PlayerState RemoteJellyfinPlayback::playbackState() const {
return m_lastSessionInfo.has_value()
? m_lastSessionInfo.value().playState()->isPaused()
? PlayerState::Paused
: PlayerState::Playing
: PlayerState::Stopped;
} }
MediaStatus RemoteJellyfinPlayback::mediaStatus() const { MediaStatus RemoteJellyfinPlayback::mediaStatus() const {
return MediaStatus::Loaded;
} }
bool RemoteJellyfinPlayback::hasNext() const { bool RemoteJellyfinPlayback::hasNext() const {
return true;
} }
bool RemoteJellyfinPlayback::hasPrevious() const { bool RemoteJellyfinPlayback::hasPrevious() const {
return true;
} }
PlaybackManagerError RemoteJellyfinPlayback::error() const { PlaybackManagerError RemoteJellyfinPlayback::error() const {
return PlaybackManagerError::NoError;
} }
const QString &RemoteJellyfinPlayback::errorString() const { const QString &RemoteJellyfinPlayback::errorString() const {
return m_sessionId;
} }
qint64 RemoteJellyfinPlayback::position() const { qint64 RemoteJellyfinPlayback::position() const {
return m_position;
} }
qint64 RemoteJellyfinPlayback::duration() const { 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 { 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 { bool RemoteJellyfinPlayback::hasAudio() const {
return false;
} }
bool RemoteJellyfinPlayback::hasVideo() const { bool RemoteJellyfinPlayback::hasVideo() const {
return false;
} }
void RemoteJellyfinPlayback::playItem(QSharedPointer<Item> item) { void RemoteJellyfinPlayback::playItem(QSharedPointer<Item> item) {
return playItemInList({item}, 0);
} }
void RemoteJellyfinPlayback::playItemInList(const QList<QSharedPointer<Item> > &items, int index) { 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() { void RemoteJellyfinPlayback::pause() {
sendPlaystateCommand(DTO::PlaystateCommand::Pause);
} }
void RemoteJellyfinPlayback::play() { void RemoteJellyfinPlayback::play() {
sendPlaystateCommand(DTO::PlaystateCommand::Unpause);
} }
void RemoteJellyfinPlayback::playItemId(const QString &id) { void RemoteJellyfinPlayback::playItemId(const QString &id) {
playItemInList({id}, 0);
} }
void RemoteJellyfinPlayback::previous() { void RemoteJellyfinPlayback::previous() {
sendPlaystateCommand(DTO::PlaystateCommand::PreviousTrack);
} }
void RemoteJellyfinPlayback::next() { void RemoteJellyfinPlayback::next() {
sendPlaystateCommand(DTO::PlaystateCommand::NextTrack);
} }
void RemoteJellyfinPlayback::goTo(int index) { void RemoteJellyfinPlayback::goTo(int index) {
@ -111,21 +148,107 @@ void RemoteJellyfinPlayback::goTo(int index) {
} }
void RemoteJellyfinPlayback::stop() { void RemoteJellyfinPlayback::stop() {
sendPlaystateCommand(DTO::PlaystateCommand::Stop);
} }
void RemoteJellyfinPlayback::seek(qint64 pos) { 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) { 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()); QSharedPointer<DTO::GeneralCommand> fullCommand = QSharedPointer<DTO::GeneralCommand>::create(command, m_apiClient.userId());
fullCommand->setArguments(arguments); 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 Model
} // NS Jellyfin } // NS Jellyfin

View file

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

View file

@ -75,25 +75,6 @@ PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent) { : QObject(parent) {
QScopedPointer<PlaybackManagerPrivate> foo(new PlaybackManagerPrivate(this)); QScopedPointer<PlaybackManagerPrivate> foo(new PlaybackManagerPrivate(this));
d_ptr.swap(foo); 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() { PlaybackManager::~PlaybackManager() {
@ -175,12 +156,61 @@ void PlaybackManager::setControllingSession(QSharedPointer<Model::ControllableSe
qCDebug(playbackManager()) << "Now controlling session " << session->name(); qCDebug(playbackManager()) << "Now controlling session " << session->name();
session->setParent(this); 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); d->m_session.swap(session);
// TODO: swap out playback manager // TODO: swap out playback manager
emit controllingSessionChanged(); emit controllingSessionChanged();
emit controllingSessionIdChanged(); emit controllingSessionIdChanged();
emit controllingSessionNameChanged(); emit controllingSessionNameChanged();
emit controllingSessionLocalChanged(); 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 { QString PlaybackManager::controllingSessionId() const {

View file

@ -26,20 +26,8 @@ namespace ViewModel {
Playlist::Playlist(Model::Playlist *data, QObject *parent) Playlist::Playlist(Model::Playlist *data, QObject *parent)
: QAbstractListModel(parent), : QAbstractListModel(parent),
m_data(data) { m_data(nullptr) {
setPlaylistModel(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);
} }
int Playlist::rowCount(const QModelIndex &parent) const { int Playlist::rowCount(const QModelIndex &parent) const {
@ -55,6 +43,41 @@ QHash<int, QByteArray> Playlist::roleNames() const {
{RoleNames::section, "section"}, {RoleNames::section, "section"},
{RoleNames::playing, "playing"}, {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 { 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/generalcommand.h>
#include <JellyfinQt/dto/generalcommandtype.h> #include <JellyfinQt/dto/generalcommandtype.h>
#include <JellyfinQt/dto/playstaterequest.h> #include <JellyfinQt/dto/playstaterequest.h>
#include <JellyfinQt/dto/sessioninfo.h>
#include <JellyfinQt/dto/useritemdatadto.h> #include <JellyfinQt/dto/useritemdatadto.h>
Q_LOGGING_CATEGORY(jellyfinWebSocket, "jellyfin.websocket"); 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; 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() { void WebSocket::onConnected() {
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
m_reconnectAttempt = 0; m_reconnectAttempt = 0;
@ -76,7 +92,6 @@ void WebSocket::onDisconnected() {
} }
void WebSocket::textMessageReceived(const QString &message) { void WebSocket::textMessageReceived(const QString &message) {
qCDebug(jellyfinWebSocket) << "message received: " << message;
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isNull() || !doc.isObject()) { if (doc.isNull() || !doc.isObject()) {
qCWarning(jellyfinWebSocket()) << "Malformed message received over WebSocket: parse error or root not an object."; 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. // Convert the type so we can use it in our enums.
QString messageType = messageRoot["MessageType"].toString(); QString messageType = messageRoot["MessageType"].toString();
qCDebug(jellyfinWebSocket) << "Message received: " << messageType;
QJsonValue data = messageRoot["Data"]; QJsonValue data = messageRoot["Data"];
if (messageType == QStringLiteral("ForceKeepAlive")) { if (messageType == QStringLiteral("ForceKeepAlive")) {
setupKeepAlive(data.toInt()); setupKeepAlive(data.toInt());
@ -136,8 +152,17 @@ void WebSocket::textMessageReceived(const QString &message) {
qCWarning(jellyfinWebSocket) << "Unparseable UserData list received: " << e->what(); 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 { } else {
qCDebug(jellyfinWebSocket) << messageType; qCDebug(jellyfinWebSocket) << "Unhandled message: " << messageType;
} }
} }