From 7a7ddc7717c7646024de5b2feb2a65ad500efcaa Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Wed, 4 Jan 2023 21:32:27 +0100 Subject: [PATCH] core: remote playback send commands and update state --- core/include/JellyfinQt/eventbus.h | 8 + .../JellyfinQt/model/controllablesession.h | 3 +- .../JellyfinQt/model/playbackmanager.h | 8 +- core/include/JellyfinQt/model/playlist.h | 5 + .../JellyfinQt/model/remotejellyfinplayback.h | 22 ++- .../include/JellyfinQt/support/jsonconvimpl.h | 2 + core/include/JellyfinQt/support/loader.h | 3 - core/include/JellyfinQt/viewmodel/playlist.h | 1 + core/include/JellyfinQt/websocket.h | 28 ++- core/src/model/controllablesession.cpp | 15 +- core/src/model/item.cpp | 4 +- core/src/model/playbackmanager.cpp | 21 ++- core/src/model/playlist.cpp | 8 + core/src/model/remotejellyfinplayback.cpp | 169 +++++++++++++++--- .../freedesktop/mediaplayer2player.cpp | 17 +- core/src/viewmodel/playbackmanager.cpp | 68 +++++-- core/src/viewmodel/playlist.cpp | 51 ++++-- core/src/websocket.cpp | 29 ++- 18 files changed, 371 insertions(+), 91 deletions(-) diff --git a/core/include/JellyfinQt/eventbus.h b/core/include/JellyfinQt/eventbus.h index 99a8888..3b94ac3 100644 --- a/core/include/JellyfinQt/eventbus.h +++ b/core/include/JellyfinQt/eventbus.h @@ -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. diff --git a/core/include/JellyfinQt/model/controllablesession.h b/core/include/JellyfinQt/model/controllablesession.h index 3860b48..a41c40a 100644 --- a/core/include/JellyfinQt/model/controllablesession.h +++ b/core/include/JellyfinQt/model/controllablesession.h @@ -105,7 +105,7 @@ private: class ControllableJellyfinSession : public ControllableSession { Q_OBJECT public: - ControllableJellyfinSession(QSharedPointer info, QObject *parent = nullptr); + ControllableJellyfinSession(QSharedPointer 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 m_data; + ApiClient &m_apiClient; }; /** diff --git a/core/include/JellyfinQt/model/playbackmanager.h b/core/include/JellyfinQt/model/playbackmanager.h index 1b49bbc..84c5e9a 100644 --- a/core/include/JellyfinQt/model/playbackmanager.h +++ b/core/include/JellyfinQt/model/playbackmanager.h @@ -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> &items, int index) = 0; + static const qint64 MS_TICK_FACTOR = 10000; +protected: + void setItem(QSharedPointer 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; diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h index 49468f2..029b3cd 100644 --- a/core/include/JellyfinQt/model/playlist.h +++ b/core/include/JellyfinQt/model/playlist.h @@ -72,6 +72,11 @@ public: */ void next(); + /** + * @brief Returns all items in the queue + */ + QList> 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(); } diff --git a/core/include/JellyfinQt/model/remotejellyfinplayback.h b/core/include/JellyfinQt/model/remotejellyfinplayback.h index 2d123c3..292bbc0 100644 --- a/core/include/JellyfinQt/model/remotejellyfinplayback.h +++ b/core/include/JellyfinQt/model/remotejellyfinplayback.h @@ -20,10 +20,17 @@ #define JELLYFIN_MODEL_REMOTEJELLYFINPLAYBACK_H #include +#include +#include +#include #include +#include #include #include +#include + +#include 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 m_lastSessionInfo; + QTimer *m_positionTimer; + qint64 m_position = 0; }; diff --git a/core/include/JellyfinQt/support/jsonconvimpl.h b/core/include/JellyfinQt/support/jsonconvimpl.h index 5f77c9d..81c0e9d 100644 --- a/core/include/JellyfinQt/support/jsonconvimpl.h +++ b/core/include/JellyfinQt/support/jsonconvimpl.h @@ -155,6 +155,8 @@ QString toString(const T &source, convertType) { 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(); diff --git a/core/include/JellyfinQt/support/loader.h b/core/include/JellyfinQt/support/loader.h index df09ccf..3afd33d 100644 --- a/core/include/JellyfinQt/support/loader.h +++ b/core/include/JellyfinQt/support/loader.h @@ -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::ResultType, // Result HttpLoader, // class diff --git a/core/include/JellyfinQt/viewmodel/playlist.h b/core/include/JellyfinQt/viewmodel/playlist.h index cd902ab..3c3c502 100644 --- a/core/include/JellyfinQt/viewmodel/playlist.h +++ b/core/include/JellyfinQt/viewmodel/playlist.h @@ -71,6 +71,7 @@ public: QVariant data(const QModelIndex &parent, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent) const override; QHash roleNames() const override; + void setPlaylistModel(Model::Playlist *data); private slots: diff --git a/core/include/JellyfinQt/websocket.h b/core/include/JellyfinQt/websocket.h index 889ba30..9c4a616 100644 --- a/core/include/JellyfinQt/websocket.h +++ b/core/include/JellyfinQt/websocket.h @@ -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); diff --git a/core/src/model/controllablesession.cpp b/core/src/model/controllablesession.cpp index 1bec6a0..275fcd0 100644 --- a/core/src/model/controllablesession.cpp +++ b/core/src/model/controllablesession.cpp @@ -5,6 +5,7 @@ #include "JellyfinQt/loader/http/session.h" #include "JellyfinQt/loader/requesttypes.h" #include +#include 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 info, QObject *parent) +ControllableJellyfinSession::ControllableJellyfinSession(const QSharedPointer 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::create(*it))); + emit sessionFound(new ControllableJellyfinSession(QSharedPointer::create(*it), *d->apiClient)); } }); d->loader->load(); diff --git a/core/src/model/item.cpp b/core/src/model/item.cpp index 30583f0..4687e68 100644 --- a/core/src/model/item.cpp +++ b/core/src/model/item.cpp @@ -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); } diff --git a/core/src/model/playbackmanager.cpp b/core/src/model/playbackmanager.cpp index 26fc8ea..5ed84e6 100644 --- a/core/src/model/playbackmanager.cpp +++ b/core/src/model/playbackmanager.cpp @@ -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) { + 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; diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp index bca2c97..ce9e899 100644 --- a/core/src/model/playlist.cpp +++ b/core/src/model/playlist.cpp @@ -104,6 +104,14 @@ void Playlist::next() { emit currentItemChanged(); } +QList> Playlist::queueAndList() const { + QList> result; + result.reserve(totalSize()); + result.append(m_queue.toList()); + result.append(m_list.toList()); + return result; +} + QSharedPointer Playlist::listAt(int index) const { if (m_shuffler->canShuffleInAdvance()) { return m_list.at(m_shuffler->itemAt(index)); diff --git a/core/src/model/remotejellyfinplayback.cpp b/core/src/model/remotejellyfinplayback.cpp index 68bc84b..d895be7 100644 --- a/core/src/model/remotejellyfinplayback.cpp +++ b/core/src/model/remotejellyfinplayback.cpp @@ -20,90 +20,127 @@ #include #include +#include #include #include +#include +#include +#include +#include + +#include 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) { - + return playItemInList({item}, 0); } void RemoteJellyfinPlayback::playItemInList(const QList > &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::create()); + } else { + Jellyfin::BaseItemDto itemData = *m_lastSessionInfo->nowPlayingItem().data(); + setItem(QSharedPointer::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 fullCommand = QSharedPointer::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 diff --git a/core/src/platform/freedesktop/mediaplayer2player.cpp b/core/src/platform/freedesktop/mediaplayer2player.cpp index 090237d..13c28d2 100644 --- a/core/src/platform/freedesktop/mediaplayer2player.cpp +++ b/core/src/platform/freedesktop/mediaplayer2player.cpp @@ -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"))) { diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 8ca1496..b1912ef 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -75,25 +75,6 @@ PlaybackManager::PlaybackManager(QObject *parent) : QObject(parent) { QScopedPointer 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(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(QSharedPointername(); 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(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(d->m_impl.data()) != nullptr; + //bool otherIsLocal = qobject_cast(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(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 { diff --git a/core/src/viewmodel/playlist.cpp b/core/src/viewmodel/playlist.cpp index 99178ba..a0dc1b6 100644 --- a/core/src/viewmodel/playlist.cpp +++ b/core/src/viewmodel/playlist.cpp @@ -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 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 { diff --git a/core/src/websocket.cpp b/core/src/websocket.cpp index a4e7638..b616ddc 100644 --- a/core/src/websocket.cpp +++ b/core/src/websocket.cpp @@ -22,6 +22,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include #include +#include #include 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 sessionInfoList = Support::fromJsonValue>(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; } }