mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-25 02:15:17 +00:00
core: remote playback send commands and update state
This commit is contained in:
parent
77cb5d5957
commit
7a7ddc7717
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(); }
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
if (!item->userData().isNull()) {
|
||||
map[QStringLiteral("xesam:lastUsed")] = item->userData()->lastPlayedDate();
|
||||
}
|
||||
|
||||
QJsonObject providers = item->providerIds();
|
||||
if (providers.contains(QStringLiteral("MusicBrainzTrack"))) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue