mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-12-25 07:15:17 +00:00
WIP: Playlist support
This commit is contained in:
parent
228f81984b
commit
fbc154fb56
|
@ -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
|
||||
|
|
|
@ -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<T> 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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
42
core/include/JellyfinQt/model/playlist.h
Normal file
42
core/include/JellyfinQt/model/playlist.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#ifndef JELLYFIN_MODEL_PLAYLIST_H
|
||||
#define JELLYFIN_MODEL_PLAYLIST_H
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
|
||||
|
||||
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> 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<ExtendedItem> list;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif // JELLYFIN_MODEL_PLAYLIST_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<int, QByteArray> roleNames() const override {
|
||||
virtual QHash<int, QByteArray> roleNames() const override {
|
||||
return {
|
||||
JFRN(jellyfinId),
|
||||
JFRN(name),
|
||||
|
|
|
@ -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<Model::Item> m_item;
|
||||
QScopedPointer<ViewModel::Item> m_displayItem = QScopedPointer<ViewModel::Item>(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
|
||||
|
|
139
core/include/JellyfinQt/viewmodel/playlist.h
Normal file
139
core/include/JellyfinQt/viewmodel/playlist.h
Normal file
|
@ -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 <optional>
|
||||
|
||||
#include <QAtomicInteger>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QQueue>
|
||||
#include <QWaitCondition>
|
||||
#include <QtMultimedia/QMediaPlaylist>
|
||||
|
||||
#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<int, QByteArray> roleNames() const override {
|
||||
QHash<int, QByteArray> 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<QString, ExtraData> 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<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams> *m_loader;
|
||||
|
||||
QMutex m_queueModifyMutex;
|
||||
QQueue<const Model::Item&> 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
|
0
core/qmldir
Normal file
0
core/qmldir
Normal file
22
core/qrc/3rdparty.xml
Normal file
22
core/qrc/3rdparty.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<includes>
|
||||
<include>
|
||||
<name>Storeman</name>
|
||||
<url>https://github.com/mentaljam/harbour-storeman/tree/f64314e7f72550faf35f95f046b52cee42501cf8</url>
|
||||
<type>SNIPPET</type>
|
||||
<license>
|
||||
<type>MIT</type>
|
||||
<copyright>Copyright (c) 2017 Petr Tsymbarovich</copyright>
|
||||
<text>licenses/MIT.txt</text>
|
||||
</license>
|
||||
</include>
|
||||
<include>
|
||||
<name>CMake-qmlplugin</name>
|
||||
<type>LIBRARY</type>
|
||||
<url>https://github.com/xarxer/cmake-qmlplugin</url>
|
||||
<license>
|
||||
<type>MIT</type>
|
||||
<copyright>Copyright (c) 2018 Pontus Sjögren</copyright>
|
||||
<text>licenses/MIT.txt</text>
|
||||
</license>
|
||||
</include>
|
||||
</includes>
|
17
core/qrc/licenses/MIT.txt
Normal file
17
core/qrc/licenses/MIT.txt
Normal file
|
@ -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.
|
|
@ -24,6 +24,7 @@ void registerTypes(const char *uri) {
|
|||
qmlRegisterType<ServerDiscoveryModel>(uri, 1, 0, "ServerDiscoveryModel");
|
||||
qmlRegisterType<ViewModel::PlaybackManager>(uri, 1, 0, "PlaybackManager");
|
||||
qmlRegisterUncreatableType<ViewModel::Item>(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties");
|
||||
qmlRegisterUncreatableType<EventBus>(uri, 1, 0, "EventBus", "Obtain one via your ApiClient");
|
||||
qmlRegisterUncreatableType<WebSocket>(uri, 1, 0, "WebSocket", "Obtain one via your ApiClient");
|
||||
|
||||
// AbstractItemModels
|
||||
|
|
0
core/src/model/playlist.cpp
Normal file
0
core/src/model/playlist.cpp
Normal file
193
core/src/viewmodel/playlist.cpp
Normal file
193
core/src/viewmodel/playlist.cpp
Normal file
|
@ -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<QString> 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<DTO::PlaybackInfoResponse> 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<DTO::MediaSourceInfo> 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
|
|
@ -19,6 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
||||
#include "JellyfinQt/websocket.h"
|
||||
|
||||
#include <JellyfinQt/dto/generalcommand.h>
|
||||
#include <JellyfinQt/dto/generalcommandtype.h>
|
||||
#include <JellyfinQt/dto/useritemdatadto.h>
|
||||
|
||||
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<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
|
||||
/*MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().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;
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue