From fbc154fb56d6b8807678b4c3bfef50137d0e35cc Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Fri, 21 May 2021 15:46:30 +0200 Subject: [PATCH] WIP: Playlist support --- core/CMakeLists.txt | 4 + core/include/JellyfinQt/apimodel.h | 6 +- core/include/JellyfinQt/eventbus.h | 6 + core/include/JellyfinQt/model/playlist.h | 42 ++++ core/include/JellyfinQt/viewmodel/itemmodel.h | 6 +- .../JellyfinQt/viewmodel/playbackmanager.h | 51 +++-- core/include/JellyfinQt/viewmodel/playlist.h | 139 +++++++++++++ core/qmldir | 0 core/qrc/3rdparty.xml | 22 ++ core/qrc/licenses/MIT.txt | 17 ++ core/src/jellyfin.cpp | 1 + core/src/model/playlist.cpp | 0 core/src/viewmodel/playlist.cpp | 193 ++++++++++++++++++ core/src/websocket.cpp | 17 +- qtquick/qml/ApiClient.qml | 2 +- qtquick/qml/pages/DetailPage.qml | 2 +- 16 files changed, 481 insertions(+), 27 deletions(-) create mode 100644 core/include/JellyfinQt/model/playlist.h create mode 100644 core/include/JellyfinQt/viewmodel/playlist.h create mode 100644 core/qmldir create mode 100644 core/qrc/3rdparty.xml create mode 100644 core/qrc/licenses/MIT.txt create mode 100644 core/src/model/playlist.cpp create mode 100644 core/src/viewmodel/playlist.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 64fa6fc..dba3f77 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -7,6 +7,7 @@ set(JellyfinQt_SOURCES # src/DTO/dto.cpp src/model/deviceprofile.cpp src/model/item.cpp + src/model/playlist.cpp src/support/jsonconv.cpp src/support/loader.cpp src/viewmodel/item.cpp @@ -14,6 +15,7 @@ set(JellyfinQt_SOURCES src/viewmodel/loader.cpp src/viewmodel/modelstatus.cpp src/viewmodel/playbackmanager.cpp + src/viewmodel/playlist.cpp src/apiclient.cpp src/apimodel.cpp src/credentialmanager.cpp @@ -28,6 +30,7 @@ list(APPEND JellyfinQt_SOURCES ${openapi_SOURCES}) set(JellyfinQt_HEADERS include/JellyfinQt/model/deviceprofile.h include/JellyfinQt/model/item.h + include/JellyfinQt/model/playlist.h include/JellyfinQt/support/jsonconv.h include/JellyfinQt/support/loader.h include/JellyfinQt/viewmodel/item.h @@ -36,6 +39,7 @@ set(JellyfinQt_HEADERS include/JellyfinQt/viewmodel/modelstatus.h include/JellyfinQt/viewmodel/propertyhelper.h include/JellyfinQt/viewmodel/playbackmanager.h + include/JellyfinQt/viewmodel/playlist.h include/JellyfinQt/apiclient.h include/JellyfinQt/apimodel.h include/JellyfinQt/credentialmanager.h diff --git a/core/include/JellyfinQt/apimodel.h b/core/include/JellyfinQt/apimodel.h index 352ed31..4d3e90b 100644 --- a/core/include/JellyfinQt/apimodel.h +++ b/core/include/JellyfinQt/apimodel.h @@ -430,7 +430,7 @@ public: } // QList-like API - T& at(int index) { return m_array.at(index); } + const T& at(int index) { return m_array.at(index); } /** * @return the amount of objects in this model. */ @@ -453,6 +453,10 @@ public: this->endInsertRows(); }; + QList mid(int pos, int length = -1) { + return m_array.mid(pos, length); + } + void removeAt(int index) { Q_ASSERT(index < size()); this->beginRemoveRows(QModelIndex(), index, index); diff --git a/core/include/JellyfinQt/eventbus.h b/core/include/JellyfinQt/eventbus.h index 2138f4b..f45bbfe 100644 --- a/core/include/JellyfinQt/eventbus.h +++ b/core/include/JellyfinQt/eventbus.h @@ -42,6 +42,12 @@ signals: * @param userData The new userData */ void itemUserDataUpdated(const QString &itemId, const DTO::UserItemDataDto &userData); + + /** + * @brief The server has requested to display an message to the user + * @param message The message to show. + */ + void displayMessage(const QString &message); }; } diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h new file mode 100644 index 0000000..c1d807d --- /dev/null +++ b/core/include/JellyfinQt/model/playlist.h @@ -0,0 +1,42 @@ +#ifndef JELLYFIN_MODEL_PLAYLIST_H +#define JELLYFIN_MODEL_PLAYLIST_H + +#include +#include +#include +#include + + +namespace Jellyfin { +namespace Model { + +// Forward declaration +class Item; + +class Playlist { +public: + explicit Playlist(); + + /// Start loading data for the next item. + void preloadNext(); + + +private: + /// Extra data about each itemId that this playlist manages + struct ExtendedItem { + QSharedPointer item; + /// The url from which this item can be streamed. + QUrl url; + /// Playsession that should be reported to Jellyfin's server. + QString playSession; + /// Text to be shown when an error occurred while fetching playback information. + QString errorText; + }; + + QVector list; +}; + +} +} + +#endif // JELLYFIN_MODEL_PLAYLIST_H diff --git a/core/include/JellyfinQt/viewmodel/itemmodel.h b/core/include/JellyfinQt/viewmodel/itemmodel.h index 03c8557..805b9be 100644 --- a/core/include/JellyfinQt/viewmodel/itemmodel.h +++ b/core/include/JellyfinQt/viewmodel/itemmodel.h @@ -162,12 +162,14 @@ public: extraType, // Hand-picked, important ones - imageTags + imageTags, + + jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now }; explicit ItemModel (QObject *parent = nullptr); - QHash roleNames() const override { + virtual QHash roleNames() const override { return { JFRN(jellyfinId), JFRN(name), diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index 0859fab..b901a03 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -94,7 +94,7 @@ public: Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) - ViewModel::Item *item() const { return m_displayItem.get(); } + ViewModel::Item *item() const { return m_displayItem; } void setApiClient(ApiClient *apiClient); QString streamUrl() const { return m_streamUrl; } @@ -134,7 +134,9 @@ signals: void errorStringChanged(const QString &newErrorString); public slots: /** - * @brief playItem Plays the item with the given id. This will construct the Jellyfin::Item internally + * @brief playItem Replaces the current queue and plays the item with the given id. + * + * This will construct the Jellyfin::Item internally * and delete it later. * @param itemId The id of the item to play. */ @@ -166,39 +168,57 @@ private slots: void updatePlaybackInfo(); private: + /// Factor to multiply with when converting from milliseconds to ticks. + const static int MS_TICK_FACTOR = 10000; + enum PlaybackInfoType { Started, Stopped, Progress }; + QTimer m_updateTimer; ApiClient *m_apiClient = nullptr; QSharedPointer m_item; - QScopedPointer m_displayItem = QScopedPointer(new ViewModel::Item()); + ViewModel::Item *m_displayItem = new ViewModel::Item(this); + // Properties for making the streaming request. QString m_streamUrl; QString m_playSessionId; + /// The index of the mediastreams of the to-be-played item containing the audio int m_audioIndex = 0; + /// The index of the mediastreams of the to-be-played item containing subtitles int m_subtitleIndex = -1; + /// The position in ticks to resume playback from qint64 m_resumePosition = 0; - qint64 m_oldPosition = 0; + /// The position in ticks the playback was stopped qint64 m_stopPosition = 0; + + /// Keeps track of latest playback position + qint64 m_oldPosition = 0; + /** + * @brief Whether to automatically open the livestream of the item; + */ + bool m_autoOpen = false; + + + // Playback-related members QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState; PlayMethod m_playMethod = Transcode; QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState; - // Pointer to the current media player. + /// Pointer to the current media player. QMediaPlayer *m_mediaPlayer = nullptr; + // There are 2 media players over here, so one is able to preload the next song + // before the other starts playing + + /// Media player 1 QMediaPlayer *m_mediaPlayer1; + /// Media player 2 QMediaPlayer *m_mediaPlayer2; ItemModel *m_queue = nullptr; int m_queueIndex = 0; bool m_resumePlayback = true; + // Helper methods void setItem(ViewModel::Item *newItem); void swapMediaPlayer(); - bool m_qmlIsParsingComponent = false; - - /** - * @brief Whether to automatically open the livestream of the item; - */ - bool m_autoOpen = false; /** * @brief Retrieves the URL of the stream to open. @@ -211,10 +231,7 @@ private: Model::Item *nextItem(); void setQueue(ItemModel *itemModel); - // Factor to multiply with when converting from milliseconds to ticks. - const static int MS_TICK_FACTOR = 10000; - enum PlaybackInfoType { Started, Stopped, Progress }; /** * @brief Posts the playback information @@ -222,10 +239,10 @@ private: void postPlaybackInfo(PlaybackInfoType type); - void classBegin() override { - m_qmlIsParsingComponent = true; - } + // QQmlParserListener interface + void classBegin() override { m_qmlIsParsingComponent = true; } void componentComplete() override; + bool m_qmlIsParsingComponent = false; }; } // NS ViewModel diff --git a/core/include/JellyfinQt/viewmodel/playlist.h b/core/include/JellyfinQt/viewmodel/playlist.h new file mode 100644 index 0000000..6e98cd0 --- /dev/null +++ b/core/include/JellyfinQt/viewmodel/playlist.h @@ -0,0 +1,139 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef JELLYFIN_VIEWMODEL_PLAYLIST +#define JELLYFIN_VIEWMODEL_PLAYLIST + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../dto/playbackinfodto.h" +#include "../loader/requesttypes.h" +#include "../loader/http/getpostedplaybackinfo.h" +#include "itemmodel.h" + +namespace Jellyfin { +namespace ViewModel { + +class ItemUrlFetcherThread; + + +/** + * @brief Playlist/queue that can be exposed to the UI. It also containts the playlist-related logic, + * which is mostly relevant + */ +class Playlist : public ItemModel { + Q_OBJECT + friend class ItemUrlFetcherThread; +public: + explicit Playlist(ApiClient *apiClient, QObject *parent = nullptr); + enum ExtraRoles { + Url = ItemModel::RoleNames::jellyfinExtendModelAfterHere + 1, + PlaySession, + ErrorText + }; + + QHash roleNames() const override { + QHash result(ItemModel::roleNames()); + result.insert(Url, "url"); + result.insert(PlaySession, "playSession"); + result.insert(ErrorText, "errorText"); + return result; + } + +private slots: + void onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex); + void onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int destinationRow); + void onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex); + void onItemsReset(); + /// Called when the fetcherThread has fetched the playback URL and playSession + void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession); + /// Called when the fetcherThread encountered an error + void onItemErrorReceived(const QString &itemId, const QString &errorString); +private: + /// Map from ItemId to ExtraData + QHash m_cache; + + ApiClient *m_apiClient; + + /// Thread that fetches the URLS asynchronously + ItemUrlFetcherThread *m_fetcherThread; +}; + +/// Thread that fetches the Item's stream URL always in the given order they were requested +class ItemUrlFetcherThread : public QThread { + Q_OBJECT +public: + ItemUrlFetcherThread(Playlist *playlist); + + /** + * @brief Adds an item to the queue of items that should be requested + * @param item The item to fetch the URL of + */ + void addItemToQueue(const Model::Item item); + +signals: + /** + * @brief Emitted when the url of the item with the itemId has been retrieved. + * @param itemId The id of the item of which the URL has been retrieved + * @param itemUrl The retrieved url + * @param playSession The playsession set by the Jellyfin Server + */ + void itemUrlFetched(QString itemId, QUrl itemUrl, QString playSession); + void itemUrlFetchError(QString itemId, QString errorString); + + void prepareLoaderRequested(QPrivateSignal); +public slots: + /** + * @brief Ask the thread nicely to stop running. + */ + void cleanlyStop(); +private slots: + void onPrepareLoader(); +protected: + void run() override; +private: + Playlist *m_parent; + Support::Loader *m_loader; + + QMutex m_queueModifyMutex; + QQueue m_queue; + + QMutex m_urlWaitConditionMutex; + /// WaitCondition on which this threads waits until an Item is put into the queue + QWaitCondition m_urlWaitCondition; + + QMutex m_waitLoaderPreparedMutex; + /// WaitCondition on which this threads waits until the loader has been prepared. + QWaitCondition m_waitLoaderPrepared; + + bool m_keepRunning = true; + bool m_loaderPrepared = false; +}; + +} // NS ViewModel +} // NS Jellyfin + +#endif //JELLYFIN_VIEWMODEL_PLAYLIST diff --git a/core/qmldir b/core/qmldir new file mode 100644 index 0000000..e69de29 diff --git a/core/qrc/3rdparty.xml b/core/qrc/3rdparty.xml new file mode 100644 index 0000000..6c79c4f --- /dev/null +++ b/core/qrc/3rdparty.xml @@ -0,0 +1,22 @@ + + + Storeman + https://github.com/mentaljam/harbour-storeman/tree/f64314e7f72550faf35f95f046b52cee42501cf8 + SNIPPET + + MIT + Copyright (c) 2017 Petr Tsymbarovich + licenses/MIT.txt + + + + CMake-qmlplugin + LIBRARY + https://github.com/xarxer/cmake-qmlplugin + + MIT + Copyright (c) 2018 Pontus Sjögren + licenses/MIT.txt + + + diff --git a/core/qrc/licenses/MIT.txt b/core/qrc/licenses/MIT.txt new file mode 100644 index 0000000..969d061 --- /dev/null +++ b/core/qrc/licenses/MIT.txt @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index d4a2630..2e6faee 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -24,6 +24,7 @@ void registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "ServerDiscoveryModel"); qmlRegisterType(uri, 1, 0, "PlaybackManager"); qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties"); + qmlRegisterUncreatableType(uri, 1, 0, "EventBus", "Obtain one via your ApiClient"); qmlRegisterUncreatableType(uri, 1, 0, "WebSocket", "Obtain one via your ApiClient"); // AbstractItemModels diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp new file mode 100644 index 0000000..e69de29 diff --git a/core/src/viewmodel/playlist.cpp b/core/src/viewmodel/playlist.cpp new file mode 100644 index 0000000..a09832f --- /dev/null +++ b/core/src/viewmodel/playlist.cpp @@ -0,0 +1,193 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include "JellyfinQt/viewmodel/playlist.h" + +namespace Jellyfin { +namespace ViewModel { + +Playlist::Playlist(ApiClient *apiClient, QObject *parent) + : ItemModel(parent), m_apiClient(apiClient), m_fetcherThread(new ItemUrlFetcherThread(this)){ + + connect(this, &QAbstractListModel::rowsInserted, this, &Playlist::onItemsAdded); + connect(this, &QAbstractListModel::rowsRemoved, this, &Playlist::onItemsRemoved); + connect(this, &QAbstractListModel::rowsMoved, this, &Playlist::onItemsMoved); + connect(this, &QAbstractListModel::modelReset, this, &Playlist::onItemsReset); + connect(m_fetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &Playlist::onItemExtraDataReceived); +} + +void Playlist::onItemsAdded(const QModelIndex &parent, int startIndex, int endIndex) { + if (parent.isValid()) return; + // Retrieve added items. + for (int i = startIndex; i <= endIndex; i++) { + m_fetcherThread->addItemToQueue(at(i)); + } +} + +void Playlist::onItemsMoved(const QModelIndex &parent, int startIndex, int endIndex, const QModelIndex &destination, int index) { + if (parent.isValid()) return; + if (destination.isValid()) return; +} + +void Playlist::onItemsRemoved(const QModelIndex &parent, int startIndex, int endIndex) { + if (parent.isValid()) return; + QSet removedItemIds; + // Assume almost all items in the playlist are unique + // If you're the kind of person who puts songs multiple time inside of a playlist: + // enjoy your needlessly reserved memory. + removedItemIds.reserve(endIndex - startIndex + 1); + // First collect all the ids of items that are going to be removed and how many of them there are + for (int i = startIndex; i <= endIndex; i++) { + removedItemIds.insert(at(i).jellyfinId()); + } + + // Look for itemIds which appear outside of the removed range + // these do not need to be removed from the cahe + for (int i = 0; i < startIndex; i++) { + const QString &id = at(i).jellyfinId(); + if (removedItemIds.contains(id)) { + removedItemIds.remove(id); + } + } + + for (int i = endIndex + 1; i < size(); i++) { + const QString &id = at(i).jellyfinId(); + if (removedItemIds.contains(id)) { + removedItemIds.remove(id); + } + } + + for (auto it = removedItemIds.cbegin(); it != removedItemIds.cend(); it++) { + m_cache.remove(*it); + } +} + +void Playlist::onItemsReset() { + +} + +void Playlist::onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession) { + m_cache.insert(itemId, ExtraData { url, playSession, QString()}); +} + +void Playlist::onItemErrorReceived(const QString &itemId, const QString &errorString) { + m_cache.insert(itemId, ExtraData {QUrl(), QString(), errorString}); +} + +ItemUrlFetcherThread::ItemUrlFetcherThread(Playlist *playlist) : + QThread(playlist), + m_parent(playlist), + m_loader(new Loader::HTTP::GetPostedPlaybackInfoLoader(playlist->m_apiClient)) { + + connect(this, &ItemUrlFetcherThread::prepareLoaderRequested, this, &ItemUrlFetcherThread::onPrepareLoader); +} + +void ItemUrlFetcherThread::addItemToQueue(const Model::Item item) { + QMutexLocker locker(&m_queueModifyMutex); + m_queue.enqueue(item); + m_urlWaitCondition.wakeOne(); +} + +void ItemUrlFetcherThread::cleanlyStop() { + m_keepRunning = false; + m_urlWaitCondition.wakeAll(); +} + +void ItemUrlFetcherThread::onPrepareLoader() { + m_loader->prepareLoad(); + m_loaderPrepared = true; + m_waitLoaderPrepared.wakeOne(); +} + +void ItemUrlFetcherThread::run() { + while (m_keepRunning) { + while(m_queue.isEmpty() && m_keepRunning) { + m_urlWaitConditionMutex.lock(); + m_urlWaitCondition.wait(&m_urlWaitConditionMutex); + } + if (!m_keepRunning) break; + + Jellyfin::Loader::GetPostedPlaybackInfoParams params; + const Model::Item& item = m_queue.dequeue(); + m_queueModifyMutex.lock(); + params.setItemId(item.jellyfinId()); + m_queueModifyMutex.unlock(); + params.setUserId(m_parent->m_apiClient->userId()); + params.setEnableDirectPlay(true); + params.setEnableDirectStream(true); + params.setEnableTranscoding(true); + + m_loaderPrepared = false; + m_loader->setParameters(params); + + // We cannot call m_loader->prepareLoad() from this thread, so we must + // emit a signal and hope for the best + emit prepareLoaderRequested(QPrivateSignal()); + while (!m_loaderPrepared) { + m_waitLoaderPreparedMutex.lock(); + m_waitLoaderPrepared.wait(&m_waitLoaderPreparedMutex); + } + + DTO::PlaybackInfoResponse response; + try { + std::optional responseOpt = m_loader->load(); + if (responseOpt.has_value()) { + response = responseOpt.value(); + } else { + qWarning() << "Cannot retrieve URL of " << params.itemId(); + continue; + } + } catch (QException e) { + qWarning() << "Cannot retrieve URL of " << params.itemId() << ": " << e.what(); + continue; + } + + QList mediaSources = response.mediaSources(); + QUrl resultingUrl; + QString playSession = response.playSessionId(); + for (int i = 0; i < mediaSources.size(); i++) { + const DTO::MediaSourceInfo &source = mediaSources.at(i); + if (source.supportsDirectPlay()) { + resultingUrl = QUrl::fromLocalFile(source.path()); + } else if (source.supportsDirectStream()) { + QString mediaType = item.mediaType(); + QUrlQuery query; + query.addQueryItem("mediaSourceId", source.jellyfinId()); + query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId()); + query.addQueryItem("api_key", m_parent->m_apiClient->token()); + query.addQueryItem("Static", "True"); + resultingUrl = QUrl(this->m_parent->m_apiClient->baseUrl() + "/" + mediaType + "/" + params.itemId() + + "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved)); + } else if (source.supportsTranscoding()) { + resultingUrl = QUrl(m_parent->m_apiClient->baseUrl() + source.transcodingUrl()); + } else { + qDebug() << "No suitable sources for item " << item.jellyfinId(); + } + if (!resultingUrl.isEmpty()) break; + } + if (resultingUrl.isEmpty()) { + qWarning() << "Could not find suitable media source for item " << params.itemId(); + emit itemUrlFetchError(item.jellyfinId(), tr("Cannot fetch stream URL")); + } + emit itemUrlFetched(item.jellyfinId(), resultingUrl, playSession); + } +} + + +} // NS ViewModel +} // NS Jellyfin diff --git a/core/src/websocket.cpp b/core/src/websocket.cpp index 8e1381d..fb38c9b 100644 --- a/core/src/websocket.cpp +++ b/core/src/websocket.cpp @@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "JellyfinQt/websocket.h" +#include +#include #include namespace Jellyfin { @@ -70,15 +72,21 @@ void WebSocket::textMessageReceived(const QString &message) { return; } QJsonObject messageRoot = doc.object(); - if (!messageRoot.contains("MessageType") || !messageRoot.contains("Data")) { - qWarning() << "Malformed message received over WebSocket: no MessageType and Data set."; + if (!messageRoot.contains("MessageType")) { + qWarning() << "Malformed message received over WebSocket: no MessageType set."; return; } // Convert the type so we can use it in our enums. QString messageTypeStr = messageRoot["MessageType"].toString(); + QJsonValue data = messageRoot["Data"]; + if (messageTypeStr == QStringLiteral("ForceKeepAlive")) { + setupKeepAlive(data.toInt()); + } else { + qDebug() << messageTypeStr; + } bool ok; - MessageType messageType = static_cast(QMetaEnum::fromType().keyToValue(messageTypeStr.toLatin1(), &ok)); + /*MessageType messageType = static_cast(QMetaEnum::fromType().keyToValue(messageTypeStr.toLatin1(), &ok)); if (!ok) { qWarning() << "Unknown message arrived: " << messageTypeStr; if (messageRoot.contains("Data")) { @@ -87,7 +95,6 @@ void WebSocket::textMessageReceived(const QString &message) { return; } - QJsonValue data = messageRoot["Data"]; qDebug() << "Received message: " << messageTypeStr; switch (messageType) { @@ -111,7 +118,7 @@ void WebSocket::textMessageReceived(const QString &message) { } break; - } + }*/ } diff --git a/qtquick/qml/ApiClient.qml b/qtquick/qml/ApiClient.qml index 4d636e4..643e0d3 100644 --- a/qtquick/qml/ApiClient.qml +++ b/qtquick/qml/ApiClient.qml @@ -3,5 +3,5 @@ import QtQuick 2.12 import nl.netsoj.chris.Jellyfin 1.0 as J J.ApiClient { - supportedCommands: [J.GeneralCommandType.Play] + supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage] } diff --git a/qtquick/qml/pages/DetailPage.qml b/qtquick/qml/pages/DetailPage.qml index b7dc2ba..9d271d7 100644 --- a/qtquick/qml/pages/DetailPage.qml +++ b/qtquick/qml/pages/DetailPage.qml @@ -53,7 +53,7 @@ Page { icon.source: ApiClient.baseUrl + "/Items/" + model.jellyfinId + "/Images/Primary?tag=" + model.tag text: model.name width: parent.width - onClicked: playbackManager.play(model.jellyfinId) + onClicked: playbackManager.playItem(model.jellyfinId) } } }