mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-04 01:42:44 +00:00
WIP: Add playlists/queues and add support for Sailfish back
This commit is contained in:
parent
fbc154fb56
commit
86672be051
89 changed files with 1637 additions and 849 deletions
|
@ -84,14 +84,14 @@ public:
|
|||
Q_PROPERTY(QStringList productionLocations READ productionLocations NOTIFY productionLocationsChanged)
|
||||
|
||||
// Handpicked, important ones
|
||||
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)
|
||||
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks NOTIFY runTimeTicksChanged)*/
|
||||
Q_PROPERTY(QString overview READ overview NOTIFY overviewChanged)
|
||||
Q_PROPERTY(int productionYear READ productionYear NOTIFY productionYearChanged)
|
||||
Q_PROPERTY(int indexNumber READ indexNumber NOTIFY indexNumberChanged)
|
||||
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd NOTIFY indexNumberEndChanged)
|
||||
Q_PROPERTY(bool isFolder READ isFolder NOTIFY isFolderChanged)
|
||||
Q_PROPERTY(QString type READ type NOTIFY typeChanged)
|
||||
Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
|
||||
/*Q_PROPERTY(QString parentBackdropItemId READ parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
|
||||
Q_PROPERTY(QStringList parentBackdropImageTags READ parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
|
||||
Q_PROPERTY(UserData *userData READ userData NOTIFY userDataChanged)
|
||||
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount NOTIFY recursiveItemCountChanged)
|
||||
|
@ -125,6 +125,12 @@ public:
|
|||
int airsBeforeSeasonNumber() const { return m_data->airsBeforeSeasonNumber().value_or(0); }
|
||||
int airsAfterSeasonNumber() const { return m_data->airsAfterSeasonNumber().value_or(999); }
|
||||
int airsBeforeEpisodeNumber() const { return m_data->airsBeforeEpisodeNumber().value_or(0); }
|
||||
QString overview() const { return m_data->overview(); }
|
||||
int productionYear() const { return m_data->productionYear().value_or(0); }
|
||||
int indexNumber() const { return m_data->indexNumber().value_or(-1); }
|
||||
int indexNumberEnd() const { return m_data->indexNumberEnd().value_or(-1); }
|
||||
bool isFolder() const { return m_data->isFolder().value_or(false); }
|
||||
QString type() const { return m_data->type(); }
|
||||
|
||||
QSharedPointer<Model::Item> data() const { return m_data; }
|
||||
void setData(QSharedPointer<Model::Item> newData);
|
||||
|
@ -192,7 +198,7 @@ public:
|
|||
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
|
||||
|
||||
QString itemId() const { return m_parameters.itemId(); }
|
||||
void setItemId(QString newItemId) { m_parameters.setItemId(newItemId); emit itemIdChanged(newItemId); }
|
||||
void setItemId(QString newItemId);
|
||||
virtual bool canReload() const override;
|
||||
|
||||
signals:
|
||||
|
|
|
@ -163,6 +163,10 @@ public:
|
|||
|
||||
// Hand-picked, important ones
|
||||
imageTags,
|
||||
imageBlurHashes,
|
||||
mediaType,
|
||||
type,
|
||||
collectionType,
|
||||
|
||||
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
|
||||
};
|
||||
|
@ -183,9 +187,14 @@ public:
|
|||
JFRN(extraType),
|
||||
// Handpicked, important ones
|
||||
JFRN(imageTags),
|
||||
JFRN(imageBlurHashes),
|
||||
JFRN(mediaType),
|
||||
JFRN(type),
|
||||
JFRN(collectionType),
|
||||
};
|
||||
}
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QSharedPointer<Model::Item> itemAt(int index);
|
||||
};
|
||||
|
||||
/*class UserItemModel : public ItemModel {
|
||||
|
|
|
@ -110,15 +110,19 @@ protected:
|
|||
void setError(QNetworkReply::NetworkError error);
|
||||
void setErrorString(const QString &newErrorString);
|
||||
|
||||
|
||||
void reloadIfNeeded() {
|
||||
if (canReload()) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
void classBegin() override {
|
||||
m_isParsing = true;
|
||||
}
|
||||
|
||||
void componentComplete() override {
|
||||
m_isParsing = false;
|
||||
if (canReload()) {
|
||||
reload();
|
||||
}
|
||||
reloadIfNeeded();
|
||||
}
|
||||
ApiClient *m_apiClient = nullptr;
|
||||
protected:
|
||||
|
@ -157,7 +161,7 @@ public:
|
|||
}
|
||||
|
||||
T *dataViewModel() const { return m_dataViewModel; }
|
||||
QObject *data() const { return m_dataViewModel; }
|
||||
QObject *data() const override { return m_dataViewModel; }
|
||||
|
||||
void reload() override {
|
||||
if (m_futureWatcher->isRunning()) return;
|
||||
|
|
|
@ -34,9 +34,13 @@
|
|||
#include <functional>
|
||||
|
||||
#include "../dto/baseitemdto.h"
|
||||
#include "../dto/playbackinfodto.h"
|
||||
#include "../dto/playmethod.h"
|
||||
#include "../loader/requesttypes.h"
|
||||
#include "../loader/http/getpostedplaybackinfo.h"
|
||||
#include "../model/playlist.h"
|
||||
#include "../support/jsonconv.h"
|
||||
#include "../viewmodel/item.h"
|
||||
|
||||
#include "../apiclient.h"
|
||||
#include "itemmodel.h"
|
||||
|
||||
|
@ -50,6 +54,9 @@ class RemoteItem;
|
|||
|
||||
namespace ViewModel {
|
||||
|
||||
// Later defined in this file
|
||||
class ItemUrlFetcherThread;
|
||||
|
||||
/**
|
||||
* @brief The PlaybackManager class manages the playback of Jellyfin items. It fetches streams based on Jellyfin items, posts
|
||||
* the current playback state to the Jellyfin Server, contains the actual media player and so on.
|
||||
|
@ -58,15 +65,10 @@ namespace ViewModel {
|
|||
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
|
||||
*/
|
||||
class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||
friend class ItemUrlFetcherThread;
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(QQmlParserStatus)
|
||||
public:
|
||||
enum PlayMethod {
|
||||
Transcode,
|
||||
Stream,
|
||||
DirectPlay
|
||||
};
|
||||
Q_ENUM(PlayMethod)
|
||||
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
|
||||
|
||||
explicit PlaybackManager(QObject *parent = nullptr);
|
||||
|
@ -77,11 +79,11 @@ public:
|
|||
Q_PROPERTY(int audioIndex MEMBER m_audioIndex NOTIFY audioIndexChanged)
|
||||
Q_PROPERTY(int subtitleIndex MEMBER m_subtitleIndex NOTIFY subtitleIndexChanged)
|
||||
Q_PROPERTY(bool resumePlayback MEMBER m_resumePlayback NOTIFY resumePlaybackChanged)
|
||||
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
|
||||
Q_PROPERTY(Jellyfin::DTO::PlayMethodClass::Value playMethod READ playMethod NOTIFY playMethodChanged)
|
||||
|
||||
// Current Item and queue informatoion
|
||||
Q_PROPERTY(ViewModel::Item *item READ item NOTIFY itemChanged)
|
||||
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
|
||||
Q_PROPERTY(QObject *item READ item NOTIFY itemChanged)
|
||||
// Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
|
||||
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
|
||||
|
||||
// Current media player related property getters
|
||||
|
@ -102,7 +104,7 @@ public:
|
|||
QObject *mediaObject() const { return m_mediaPlayer; }
|
||||
qint64 position() const { return m_mediaPlayer->position(); }
|
||||
qint64 duration() const { return m_mediaPlayer->duration(); }
|
||||
ItemModel *queue() const { return m_queue; }
|
||||
//ItemModel *queue() const { return m_queue; }
|
||||
int queueIndex() const { return m_queueIndex; }
|
||||
|
||||
// Current media player related property getters
|
||||
|
@ -112,7 +114,7 @@ public:
|
|||
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
|
||||
QString errorString() const { return m_mediaPlayer->errorString(); }
|
||||
signals:
|
||||
void itemChanged(BaseItemDto *newItemId);
|
||||
void itemChanged(ViewModel::Item *newItemId);
|
||||
void streamUrlChanged(const QString &newStreamUrl);
|
||||
void autoOpenChanged(bool autoOpen);
|
||||
void audioIndexChanged(int audioIndex);
|
||||
|
@ -125,7 +127,7 @@ signals:
|
|||
void mediaObjectChanged(QObject *newMediaObject);
|
||||
void positionChanged(qint64 newPosition);
|
||||
void durationChanged(qint64 newDuration);
|
||||
void queueChanged(ItemModel *newQue);
|
||||
//void queueChanged(ItemModel *newQue);
|
||||
void queueIndexChanged(int newIndex);
|
||||
void playbackStateChanged(QMediaPlayer::State newState);
|
||||
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
|
||||
|
@ -138,9 +140,9 @@ public slots:
|
|||
*
|
||||
* This will construct the Jellyfin::Item internally
|
||||
* and delete it later.
|
||||
* @param itemId The id of the item to play.
|
||||
* @param item The item to play.
|
||||
*/
|
||||
void playItem(const QString &itemId);
|
||||
void playItem(Item *item);
|
||||
void playItemInList(ItemModel *itemList, int index);
|
||||
void play() { m_mediaPlayer->play(); }
|
||||
void pause() { m_mediaPlayer->pause(); }
|
||||
|
@ -162,24 +164,43 @@ private slots:
|
|||
void mediaPlayerPositionChanged(qint64 position);
|
||||
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
|
||||
void mediaPlayerError(QMediaPlayer::Error error);
|
||||
void mediaPlayerDurationChanged(qint64 newDuration);
|
||||
/**
|
||||
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
|
||||
*/
|
||||
void updatePlaybackInfo();
|
||||
|
||||
/// Called when the fetcherThread has fetched the playback URL and playSession
|
||||
void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession,
|
||||
// Fully specify class to please MOC
|
||||
Jellyfin::DTO::PlayMethodClass::Value playMethod);
|
||||
/// Called when the fetcherThread encountered an error
|
||||
void onItemErrorReceived(const QString &itemId, const QString &errorString);
|
||||
void onDestroyed();
|
||||
|
||||
private:
|
||||
/// Factor to multiply with when converting from milliseconds to ticks.
|
||||
const static int MS_TICK_FACTOR = 10000;
|
||||
enum PlaybackInfoType { Started, Stopped, Progress };
|
||||
|
||||
/// Timer used to update the play progress on the Jellyfin server
|
||||
QTimer m_updateTimer;
|
||||
/// Timer used to notify ourselves when we need to preload the next item
|
||||
QTimer m_preloadTimer;
|
||||
|
||||
ApiClient *m_apiClient = nullptr;
|
||||
/// The currently playing item
|
||||
QSharedPointer<Model::Item> m_item;
|
||||
/// The item that will be played next
|
||||
QSharedPointer<Model::Item> m_nextItem;
|
||||
/// The currently played item that will be shown in the GUI
|
||||
ViewModel::Item *m_displayItem = new ViewModel::Item(this);
|
||||
|
||||
// Properties for making the streaming request.
|
||||
QString m_streamUrl;
|
||||
QString m_nextStreamUrl;
|
||||
QString m_playSessionId;
|
||||
QString m_nextPlaySessionId;
|
||||
/// 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
|
||||
|
@ -196,10 +217,11 @@ private:
|
|||
*/
|
||||
bool m_autoOpen = false;
|
||||
|
||||
|
||||
// Playback-related members
|
||||
ItemUrlFetcherThread *m_urlFetcherThread;
|
||||
|
||||
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
||||
PlayMethod m_playMethod = Transcode;
|
||||
PlayMethod m_playMethod = PlayMethod::Transcode;
|
||||
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
||||
/// Pointer to the current media player.
|
||||
QMediaPlayer *m_mediaPlayer = nullptr;
|
||||
|
@ -211,28 +233,18 @@ private:
|
|||
QMediaPlayer *m_mediaPlayer1;
|
||||
/// Media player 2
|
||||
QMediaPlayer *m_mediaPlayer2;
|
||||
ItemModel *m_queue = nullptr;
|
||||
|
||||
Model::Playlist *m_queue = nullptr;
|
||||
int m_queueIndex = 0;
|
||||
bool m_resumePlayback = true;
|
||||
|
||||
// Helper methods
|
||||
void setItem(ViewModel::Item *newItem);
|
||||
void setItem(QSharedPointer<Model::Item> newItem);
|
||||
void swapMediaPlayer();
|
||||
|
||||
|
||||
/**
|
||||
* @brief Retrieves the URL of the stream to open.
|
||||
*/
|
||||
void fetchStreamUrl(const Model::Item *item, bool autoOpen, const FetchCallback &callback);
|
||||
void fetchAndSetStreamUrl(const Model::Item *item);
|
||||
void setStreamUrl(const QString &streamUrl);
|
||||
void setStreamUrl(const QUrl &streamUrl);
|
||||
void setPlaybackState(QMediaPlayer::State newState);
|
||||
|
||||
Model::Item *nextItem();
|
||||
void setQueue(ItemModel *itemModel);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Posts the playback information
|
||||
*/
|
||||
|
@ -243,6 +255,60 @@ private:
|
|||
void classBegin() override { m_qmlIsParsingComponent = true; }
|
||||
void componentComplete() override;
|
||||
bool m_qmlIsParsingComponent = false;
|
||||
|
||||
/// Time in ms at what moment this playbackmanager should start loading the next item.
|
||||
const qint64 PRELOAD_DURATION = 15 * 1000;
|
||||
};
|
||||
|
||||
/// Thread that fetches the Item's stream URL always in the given order they were requested
|
||||
class ItemUrlFetcherThread : public QThread {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ItemUrlFetcherThread(PlaybackManager *manager);
|
||||
|
||||
/**
|
||||
* @brief Adds an item to the queue of items that should be requested
|
||||
* @param item The item to fetch the URL of
|
||||
*/
|
||||
void addItemToQueue(QSharedPointer<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, Jellyfin::DTO::PlayMethodClass::Value playMethod);
|
||||
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:
|
||||
PlaybackManager *m_parent;
|
||||
Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams> *m_loader;
|
||||
|
||||
QMutex m_queueModifyMutex;
|
||||
QQueue<QSharedPointer<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
|
||||
|
|
|
@ -29,109 +29,30 @@
|
|||
#include <QWaitCondition>
|
||||
#include <QtMultimedia/QMediaPlaylist>
|
||||
|
||||
#include "../dto/playbackinfodto.h"
|
||||
#include "../loader/requesttypes.h"
|
||||
#include "../loader/http/getpostedplaybackinfo.h"
|
||||
#include "../apiclient.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 {
|
||||
/*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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue