1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-04 01:42:44 +00:00

Moved playback logic to C++-side (and refractoring)

This commit is contained in:
Chris Josten 2021-02-20 23:20:39 +01:00
parent 895731ae38
commit f7bca333c8
35 changed files with 1063 additions and 449 deletions

View file

@ -46,6 +46,7 @@ class JsonSerializable : public QObject {
Q_OBJECT
public:
Q_INVOKABLE JsonSerializable(QObject *parent);
virtual ~JsonSerializable();
/**
* @brief Sets this objects properties based on obj.
@ -56,7 +57,7 @@ public:
private:
QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root);
QJsonValue variantToJson(const QVariant var) const;
QVariant deserializeQobject(const QJsonObject &obj, const QMetaProperty &prop);
QVariant deserializeQObject(const QJsonObject &obj, const QMetaProperty &prop);
/**
* @brief Sets the first letter of the string to lower case (to make it camelCase).
@ -72,6 +73,8 @@ private:
static QString toPascalCase(QString st);
static const QRegularExpression m_listExpression;
static const QRegularExpression m_hashExpression;
static int findTypeIdForProperty(QString type);
/**
* @brief Qt is doing weird. I'll keep track of the metatypes myself.
*/
@ -107,17 +110,20 @@ public:
Q_PROPERTY(Status status READ status NOTIFY statusChanged STORED false)
Q_PROPERTY(QNetworkReply::NetworkError error READ error NOTIFY errorChanged STORED false)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged STORED false)
Q_PROPERTY(QStringList extraFields MEMBER m_extraFields WRITE setExtraFields NOTIFY extraFieldsChanged STORED false)
Status status() const { return m_status; }
QNetworkReply::NetworkError error() const { return m_error; }
QString errorString() const { return m_errorString; }
void setApiClient(ApiClient *newApiClient);
void setExtraFields(const QStringList &extraFields);
signals:
void statusChanged(Status newStatus);
void apiClientChanged(ApiClient *newApiClient);
void errorChanged(QNetworkReply::NetworkError newError);
void errorStringChanged(QString newErrorString);
void extraFieldsChanged(const QStringList &newExtraFields);
/**
* @brief Convenience signal for status == RemoteData.Ready.
*/
@ -159,6 +165,7 @@ private:
Status m_status = Uninitialised;
QNetworkReply::NetworkError m_error = QNetworkReply::NoError;
QString m_errorString;
QStringList m_extraFields;
};
} // NS DTO

View file

@ -93,11 +93,13 @@ public:
Q_PROPERTY(int productionYear READ productionYear WRITE setProductionYear NOTIFY productionYearChanged)
Q_PROPERTY(int indexNumber READ indexNumber WRITE setIndexNumber NOTIFY indexNumberChanged)
Q_PROPERTY(int indexNumberEnd READ indexNumberEnd WRITE setIndexNumberEnd NOTIFY indexNumberEndChanged)
Q_PROPERTY(int parentIndexNumber READ parentIndexNumber WRITE setParentIndexNumber NOTIFY parentIndexNumberChanged)
Q_PROPERTY(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged)
Q_PROPERTY(QString parentID MEMBER m_parentId READ parentId NOTIFY parentIdChanged)
Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged)
Q_PROPERTY(QString parentBackdropItemId MEMBER m_parentBackdropItemId NOTIFY parentBackdropItemIdChanged)
Q_PROPERTY(QStringList parentBackdropImageTags MEMBER m_parentBackdropImageTags NOTIFY parentBackdropImageTagsChanged)
Q_PROPERTY(UserData *userData MEMBER m_userData NOTIFY userDataChanged)
Q_PROPERTY(UserData *userData READ userData WRITE setUserData NOTIFY userDataChanged)
Q_PROPERTY(int recursiveItemCount READ recursiveItemCount WRITE setRecursiveItemCount NOTIFY recursiveItemCountChanged)
Q_PROPERTY(int childCount READ childCount WRITE setChildCount NOTIFY childCountChanged)
Q_PROPERTY(QString albumArtist MEMBER m_albumArtist NOTIFY albumArtistChanged)
@ -147,8 +149,19 @@ public:
void setIndexNumber(int newIndexNumber) { m_indexNumber = std::optional<int>(newIndexNumber); emit indexNumberChanged(newIndexNumber); }
int indexNumberEnd() const { return m_indexNumberEnd.value_or(-1); }
void setIndexNumberEnd(int newIndexNumberEnd) { m_indexNumberEnd = std::optional<int>(newIndexNumberEnd); emit indexNumberEndChanged(newIndexNumberEnd); }
int parentIndexNumber() const { return m_parentIndexNumber.value_or(-1); }
void setParentIndexNumber(int newParentIndexNumber) { m_parentIndexNumber= std::optional<int>(newParentIndexNumber); emit parentIndexNumberChanged(newParentIndexNumber); }
bool isFolder() const { return m_isFolder.value_or(false); }
void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); }
QString parentId() const { return m_parentId; }
UserData *userData() const { return m_userData; }
void setUserData(UserData *newUserData) {
if (m_userData != nullptr) {
m_userData->deleteLater();
}
m_userData = newUserData;
emit userDataChanged(newUserData);
}
int recursiveItemCount() const { return m_recursiveItemCount.value_or(-1); }
void setRecursiveItemCount(int newRecursiveItemCount) { m_recursiveItemCount = newRecursiveItemCount; emit recursiveItemCountChanged(newRecursiveItemCount); }
int childCount() const { return m_childCount.value_or(-1); }
@ -193,6 +206,7 @@ signals:
void indexNumberChanged(int newIndexNumber);
void indexNumberEndChanged(int newIndexNumberEnd);
void isFolderChanged(bool newIsFolder);
void parentIdChanged(const QString &newParentId);
void typeChanged(const QString &newType);
void parentBackdropItemIdChanged();
void parentBackdropImageTagsChanged();
@ -213,7 +227,7 @@ signals:
void heightChanged(int newHeight);
public slots:
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
void onUserDataChanged(const QString &itemId, UserData *userData);
protected:
// Overrides
QString getDataUrl() const override;
@ -251,7 +265,9 @@ protected:
std::optional<int> m_productionYear = std::nullopt;
std::optional<int> m_indexNumber = std::nullopt;
std::optional<int> m_indexNumberEnd = std::nullopt;
std::optional<int> m_parentIndexNumber = std::nullopt;
std::optional<bool> m_isFolder = std::nullopt;
QString m_parentId;
QString m_type;
QString m_parentBackdropItemId;
QStringList m_parentBackdropImageTags;

View file

@ -35,7 +35,6 @@ public:
Q_INVOKABLE explicit MediaStream(QObject *parent = nullptr);
MediaStream(const MediaStream &other);
bool operator==(const MediaStream &other);
virtual ~MediaStream() { qDebug() << "MediaStream destroyed"; }
enum MediaStreamType {
Undefined,

View file

@ -33,26 +33,38 @@ class User : public RemoteData {
public:
Q_INVOKABLE User(QObject *parent = nullptr);
Q_PROPERTY(QString userId MEMBER m_userId WRITE setUserId NOTIFY userIdChanged)
Q_PROPERTY(QString jellyfinId MEMBER m_jellyfinId WRITE setJellyfinId NOTIFY jellyfinIdChanged)
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
Q_PROPERTY(QString primaryImageTag MEMBER m_primaryImageTag NOTIFY primaryImageTagChanged)
Q_PROPERTY(bool hasPassword MEMBER m_hasPassword NOTIFY hasPasswordChanged)
Q_PROPERTY(bool hasConfiguredPassword MEMBER m_hasConfiguredPassword NOTIFY hasConfiguredPasswordChanged)
Q_PROPERTY(bool hasConfiguredEasyPassword MEMBER m_hasConfiguredEasyPassword NOTIFY hasConfiguredEasyPasswordChanged)
void setUserId(const QString &newUserId) {
this->m_userId = newUserId;
emit userIdChanged(newUserId);
reload();
void setJellyfinId(const QString &newJellyfinId) {
if (m_jellyfinId != newJellyfinId) {
this->m_jellyfinId = newJellyfinId;
emit jellyfinIdChanged(newJellyfinId);
reload();
}
}
signals:
void userIdChanged(const QString &newUserId);
void nameChanged(const QString &newName);
void jellyfinIdChanged(const QString &newJellyfinId);
void primaryImageTagChanged(const QString &newPrimaryImageTag);
void hasPasswordChanged(bool newHasPasswordChanged);
void hasConfiguredPasswordChanged(bool newHasConfiguredPasswordChanged);
void hasConfiguredEasyPasswordChanged(bool newHasConfiguredEasyPasswordChanged);
protected:
QString getDataUrl() const override;
bool canReload() const override;
private:
QString m_userId;
QString m_name;
QString m_jellyfinId;
QString m_primaryImageTag;
bool m_hasPassword;
bool m_hasConfiguredPassword;
bool m_hasConfiguredEasyPassword;
};
} // NS DTO

View file

@ -67,7 +67,7 @@ signals:
void playedChanged(bool newPlayed);
public slots:
void updateOnServer();
void onUpdated(QSharedPointer<UserData> other);
void onUpdated(UserData *other);
private:
std::optional<double> m_playedPercentage = std::nullopt;
qint64 m_playbackPositionTicks = 0;

View file

@ -162,8 +162,9 @@ signals:
* @param userData The new user data
*
* Note: only Jellyfin::UserData should connect to this signal, they will update themselves!
* Note: the userData is only valid during this callback, afterwards it is deleted!
*/
void userDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
void userDataChanged(const QString &itemId, UserData *userData);
public slots:
/**
@ -192,7 +193,7 @@ public slots:
protected slots:
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> newData);
void onUserDataChanged(const QString &itemId, UserData *newData);
protected:
/**

View file

@ -27,6 +27,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <QJsonDocument>
#include <QJsonObject>
#include <QtQml>
#include <QQmlParserStatus>
#include <QVariant>
#include "apiclient.h"
@ -35,9 +36,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
namespace Jellyfin {
namespace DTO {
class Item;
class JsonSerializable;
class User;
}
class SortOptions : public QObject{
class SortOptions : public QObject {
Q_OBJECT
public:
explicit SortOptions (QObject *parent = nullptr) : QObject(parent) {}
@ -61,6 +64,134 @@ public:
Q_ENUM(SortBy)
};
/**
* Q_OBJECT does not support template classes. This base class declares the
* Q_OBJECT related properties and signals.
*/
class BaseApiModel : public QAbstractListModel, public QQmlParserStatus {
Q_OBJECT
public:
explicit BaseApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent = nullptr);
enum ModelStatus {
Uninitialised,
Loading,
Ready,
Error,
LoadingMore
};
Q_ENUM(ModelStatus)
enum SortOrder {
Unspecified,
Ascending,
Descending
};
Q_ENUM(SortOrder)
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient NOTIFY apiClientChanged)
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
// Query properties
Q_PROPERTY(int limit MEMBER m_limit NOTIFY limitChanged)
Q_PROPERTY(QList<QString> sortBy MEMBER m_sortBy NOTIFY sortByChanged)
Q_PROPERTY(QList<QString> fields MEMBER m_fields NOTIFY fieldsChanged)
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
ModelStatus status() const { return m_status; }
void setApiClient(ApiClient *newApiClient);
void setLimit(int newLimit);
// From AbstractListModel, gets implemented in ApiModel<T>
virtual int rowCount(const QModelIndex &index) const override = 0;
virtual QHash<int, QByteArray> roleNames() const override = 0;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override = 0;
virtual bool canFetchMore(const QModelIndex &parent) const override = 0;
virtual void fetchMore(const QModelIndex &parent) override = 0;
signals:
void ready();
void apiClientChanged(ApiClient *newApiClient);
void statusChanged(ModelStatus newStatus);
void limitChanged(int newLimit);
void sortByChanged(QList<QString> newSortOrder);
void sortOrderChanged(SortOrder newSortOrder);
void fieldsChanged(QList<QString> newFields);
public slots:
/**
* @brief (Re)loads the data into this model. This might make a network request.
*/
void reload();
protected:
enum LoadType {
RELOAD,
LOAD_MORE
};
ApiClient *m_apiClient = nullptr;
bool m_isBeingParsed = false;
// Per-model specific settings.
QString m_path;
bool m_hasRecordResponse;
bool m_addUserId;
bool padding; bool padding2;
// Query/record controlling properties
int m_limit = -1;
int m_startIndex = 0;
int m_totalRecordCount = 0;
const int DEFAULT_LIMIT = 100;
// Query properties
QList<QString> m_fields = {};
QList<QString> m_sortBy = {};
SortOrder m_sortOrder = Unspecified;
// State properties.
ModelStatus m_status = Uninitialised;
void setStatus(ModelStatus newStatus) {
if (this->m_status != newStatus) {
this->m_status = newStatus;
emit this->statusChanged(newStatus);
if (m_status == Ready) {
emit ready();
}
}
}
void load(LoadType loadType);
virtual void setModelData(QJsonArray &data) = 0;
virtual void appendModelData(QJsonArray &data) = 0;
/**
* @brief Adds parameters to the query
* @param query The query to add parameters to
*
* This method is intended to be overrided by subclasses. It gets called
* before a request is made to the server and can be used to enable
* query types specific for a certain model to be available.
*
* Make sure to call the method in the superclass as well!
*/
virtual void addQueryParameters(QUrlQuery &query);
/**
* @brief Replaces placeholders in an URL.
* @param path The path in which placeholders should be replaced.
*
* This method is intended to be overrided by subclasses. It gets called
* before a request is made to the server and can be used to enable
* query types specific for a certain model to be available.
*
* Make sure to call the method in the superclass as well!
*/
virtual void replacePathPlaceholders(QString &path);
virtual void classBegin() override;
virtual void componentComplete() override;
};
/**
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
@ -86,25 +217,9 @@ public:
* The model will have roleNames for "name" and "id".
*
*/
class ApiModel : public QAbstractListModel {
Q_OBJECT
template <typename T>
class ApiModel : public BaseApiModel {
public:
enum ModelStatus {
Uninitialised,
Loading,
Ready,
Error,
LoadingMore
};
Q_ENUM(ModelStatus)
enum SortOrder {
Unspecified,
Ascending,
Descending
};
Q_ENUM(SortOrder)
/**
* @brief Creates a new basemodel
* @param path The path (relative to the baseUrl of JellyfinApiClient) to make the call to.
@ -134,22 +249,6 @@ public:
* responseHasRecords should be true
*/
explicit ApiModel(QString path, bool responseHasRecords, bool passUserId = false, QObject *parent = nullptr);
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient NOTIFY apiClientChanged)
Q_PROPERTY(ModelStatus status READ status NOTIFY statusChanged)
// Query properties
Q_PROPERTY(int limit MEMBER m_limit NOTIFY limitChanged)
Q_PROPERTY(QString parentId MEMBER m_parentId NOTIFY parentIdChanged)
Q_PROPERTY(QList<QString> sortBy MEMBER m_sortBy NOTIFY sortByChanged)
Q_PROPERTY(QList<QString> fields MEMBER m_fields NOTIFY fieldsChanged)
Q_PROPERTY(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged)
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
Q_PROPERTY(bool recursive MEMBER m_recursive)
Q_PROPERTY(SortOrder sortOrder MEMBER m_sortOrder NOTIFY sortOrderChanged)
// Path properties
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
// Standard QAbstractItemModel overrides
int rowCount(const QModelIndex &index) const override {
@ -161,12 +260,18 @@ public:
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
ModelStatus status() const { return m_status; }
// Helper methods
template<typename QEnum>
QString enumToString (const QEnum anEnum) { return QVariant::fromValue(anEnum).toString(); }
// QList-like API
T* at(int index) { return m_array.at(index); }
int size() { return rowCount(QModelIndex()); }
void insert(int index, T* object);
void append(T* object) { insert(size(), object); }
void removeAt(int index);
void removeOne(T* object);
template<typename QEnum>
QString enumListToString (const QList<QEnum> enumList) {
QString result;
@ -176,74 +281,20 @@ public:
return result;
}
signals:
void apiClientChanged(ApiClient *newApiClient);
void statusChanged(ModelStatus newStatus);
void limitChanged(int newLimit);
void parentIdChanged(QString newParentId);
void sortByChanged(QList<QString> newSortOrder);
void sortOrderChanged(SortOrder newSortOrder);
void showChanged(QString newShow);
void seasonIdChanged(QString newSeasonId);
void fieldsChanged(QList<QString> newFields);
void imageTypesChanged(QList<QString> newImageTypes);
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
public slots:
/**
* @brief (Re)loads the data into this model. This might make a network request.
*/
void reload();
protected:
enum LoadType {
RELOAD,
LOAD_MORE
};
void load(LoadType loadType);
/**
* @brief Adds parameters to the query
* @param query The query to add parameters to
*
* This method is intended to be overrided by subclasses. It gets called
* before a request is made to the server and can be used to enable
* query types specific for a certain model to be available.
*/
virtual void addQueryParameters(QUrlQuery &query);
ApiClient *m_apiClient = nullptr;
ModelStatus m_status = Uninitialised;
QString m_path;
QJsonArray m_array;
bool m_hasRecordResponse;
// Path properties
QString m_show;
// Query/record controlling properties
int m_limit = -1;
int m_startIndex = 0;
int m_totalRecordCount = 0;
const int DEFAULT_LIMIT = 100;
// Query properties
bool m_addUserId = false;
QString m_parentId;
QString m_seasonId;
QList<QString> m_fields = {};
QList<QString> m_imageTypes = {};
QList<QString> m_sortBy = {};
QList<QString> m_includeItemTypes = {};
SortOrder m_sortOrder = Unspecified;
bool m_recursive = false;
// AbstractItemModel bookkeeping
QHash<int, QByteArray> m_roles;
void setStatus(ModelStatus newStatus) {
this->m_status = newStatus;
emit this->statusChanged(newStatus);
}
// Helper methods.
T *deserializeResult(QJsonValueRef source);
virtual void addQueryParameters(QUrlQuery &query) override;
virtual void replacePathPlaceholders(QString &path) override;
virtual void setModelData(QJsonArray &data) override;
virtual void appendModelData(QJsonArray &data) override;
// Model-specific properties.
QList<T*> m_array;
private:
/**
@ -256,7 +307,7 @@ private:
/**
* @brief List of the public users on the server.
*/
class PublicUserModel : public ApiModel {
class PublicUserModel : public ApiModel<User> {
public:
explicit PublicUserModel (QObject *parent = nullptr);
};
@ -266,15 +317,54 @@ public:
*
* Listens for updates in the library and updates the model accordingly.
*/
class ItemModel : public ApiModel {
class ItemModel : public ApiModel<Item> {
Q_OBJECT
public:
explicit ItemModel (QString path, bool responseHasRecords, bool replaceUser, QObject *parent = nullptr);
// Query parameters
Q_PROPERTY(QString parentId MEMBER m_parentId WRITE setParentId NOTIFY parentIdChanged)
Q_PROPERTY(QString seasonId MEMBER m_seasonId NOTIFY seasonIdChanged)
Q_PROPERTY(QList<QString> imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged)
Q_PROPERTY(QList<QString> includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged)
Q_PROPERTY(bool recursive MEMBER m_recursive)
QList<QString> m_includeItemTypes = {};
// Path properties
Q_PROPERTY(QString show MEMBER m_show NOTIFY showChanged)
void setParentId(const QString &parentId) {
m_parentId = parentId;
emit parentIdChanged(m_parentId);
}
signals:
// Query property signals
void parentIdChanged(QString newParentId);
void seasonIdChanged(QString newSeasonId);
void imageTypesChanged(QList<QString> newImageTypes);
void includeItemTypesChanged(const QList<QString> &newIncludeItemTypes);
// Path property signals
void showChanged(QString newShow);
public slots:
void onUserDataChanged(const QString &itemId, QSharedPointer<DTO::UserData> userData);
void onUserDataChanged(const QString &itemId, DTO::UserData *userData);
protected:
virtual void addQueryParameters(QUrlQuery &query) override;
virtual void replacePathPlaceholders(QString &path) override;
private:
// Path properties
QString m_show;
// Query parameters
QString m_parentId;
QString m_seasonId;
QList<QString> m_imageTypes = {};
bool m_recursive = false;
};
class UserViewModel : public ApiModel {
//template<>
//void ApiModel<Item>::apiClientChanged();
class UserViewModel : public ItemModel {
public:
explicit UserViewModel (QObject *parent = nullptr);
};

View file

@ -31,6 +31,7 @@ namespace Jellyfin {
namespace JsonHelper {
void convertToCamelCase(QJsonValueRef val);
void convertToCamelCase(QJsonValue &val);
QString convertToCamelCaseHelper(const QString &str);
};

View file

@ -20,29 +20,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#ifndef JELLYFIN_MEDIA_SOURCE_H
#define JELLYFIN_MEDIA_SOURCE_H
#include <QAbstractItemModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QFuture>
#include <QObject>
#include <QtGlobal>
#include <QUrlQuery>
#include <QVariant>
#include <QUrlQuery>
#include <QtMultimedia/QMediaPlayer>
#include <QtMultimedia/QMediaPlaylist>
#include <functional>
#include "JellyfinQt/DTO/item.h"
#include "apiclient.h"
namespace Jellyfin {
// Forward declaration of Jellyfin::ApiClient found in jellyfinapiclient.h
class ApiClient;
class ItemModel;
using namespace DTO;
/**
* @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 and so on.
* the current playback state to the Jellyfin Server, contains the actual media player and so on.
*
* The PlaybackManager actually keeps two mediaPlayers, m_mediaPlayer1 and m_mediaPlayer2. When one is playing, the other is
* preloading the next item in the queue. The current media player is pointed to by m_mediaPlayer.
*/
class PlaybackManager : public QObject, public QQmlParserStatus {
Q_OBJECT
@ -54,28 +63,49 @@ public:
DirectPlay
};
Q_ENUM(PlayMethod)
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
explicit PlaybackManager(QObject *parent = nullptr);
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
Q_PROPERTY(Item *item READ item WRITE setItem NOTIFY itemChanged)
Q_PROPERTY(QString streamUrl READ streamUrl NOTIFY streamUrlChanged)
Q_PROPERTY(bool autoOpen MEMBER m_autoOpen NOTIFY autoOpenChanged)
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(QObject* mediaPlayer READ mediaPlayer WRITE setMediaPlayer NOTIFY mediaPlayerChanged)
Q_PROPERTY(PlayMethod playMethod READ playMethod NOTIFY playMethodChanged)
Item *item() const { return m_item; }
void setItem(Item *newItem);
// Current Item and queue informatoion
Q_PROPERTY(Item *item READ item NOTIFY itemChanged)
Q_PROPERTY(QAbstractItemModel *queue READ queue NOTIFY queueChanged)
Q_PROPERTY(int queueIndex READ queueIndex NOTIFY queueIndexChanged)
QObject *mediaPlayer() const {
return m_qmlMediaPlayer;
}
void setMediaPlayer(QObject *qmlMediaPlayer);
// Current media player related property getters
Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
Q_PROPERTY(QMediaPlayer::Error error READ error NOTIFY errorChanged)
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
Q_PROPERTY(bool hasVideo READ hasVideo NOTIFY hasVideoChanged)
Q_PROPERTY(QObject* mediaObject READ mediaObject NOTIFY mediaObjectChanged)
Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged)
Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged)
Q_PROPERTY(qint64 position READ position NOTIFY positionChanged)
Item *item() const { return m_item; }
QString streamUrl() const { return m_streamUrl; }
PlayMethod playMethod() const { return m_playMethod; }
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; }
int queueIndex() const { return m_queueIndex; }
// Current media player related property getters
QMediaPlayer::State playbackState() const { return m_playbackState; }
QMediaPlayer::MediaStatus mediaStatus() const { return m_mediaPlayer->mediaStatus(); }
bool hasVideo() const { return m_mediaPlayer->isVideoAvailable(); }
QMediaPlayer::Error error () const { return m_mediaPlayer->error(); }
QString errorString() const { return m_mediaPlayer->errorString(); }
signals:
void itemChanged(Item *newItemId);
void streamUrlChanged(const QString &newStreamUrl);
@ -86,14 +116,29 @@ signals:
void resumePlaybackChanged(bool newResumePlayback);
void playMethodChanged(PlayMethod newPlayMethod);
// Current media player related property signals
void mediaObjectChanged(QObject *newMediaObject);
void positionChanged(qint64 newPosition);
void durationChanged(qint64 newDuration);
void queueChanged(ItemModel *newQue);
void queueIndexChanged(int newIndex);
void playbackStateChanged(QMediaPlayer::State newState);
void mediaStatusChanged(QMediaPlayer::MediaStatus newMediaStatus);
void hasVideoChanged(bool newHasVideo);
void errorChanged(QMediaPlayer::Error newError);
void errorStringChanged(const QString &newErrorString);
public slots:
void updatePlaybackInfo();
/**
* @brief playItem 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.
*/
void playItem(const QString &itemId);
void playItemInList(ItemModel *itemList, int index);
void play() { m_mediaPlayer->play(); }
void pause() { m_mediaPlayer->pause(); }
void seek(qint64 pos) { m_mediaPlayer->setPosition(pos); }
void stop() { m_mediaPlayer->stop(); }
/**
* @brief previous Play the previous track in the current playlist.
@ -104,10 +149,16 @@ public slots:
* @brief next Play the next track in the current playlist.
*/
void next();
private slots:
void mediaPlayerStateChanged(QMediaPlayer::State newState);
void mediaPlayerPositionChanged(qint64 position);
void mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus);
void mediaPlayerError(QMediaPlayer::Error error);
/**
* @brief updatePlaybackInfo Updates the Jellyfin server with the current playback progress etc.
*/
void updatePlaybackInfo();
private:
QTimer m_updateTimer;
@ -122,10 +173,19 @@ private:
qint64 m_stopPosition = 0;
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
PlayMethod m_playMethod = Transcode;
QObject *m_qmlMediaPlayer = nullptr;
QMediaPlayer * m_mediaPlayer = nullptr;
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
// Pointer to the current media player.
QMediaPlayer *m_mediaPlayer = nullptr;
QMediaPlayer *m_mediaPlayer1;
QMediaPlayer *m_mediaPlayer2;
ItemModel *m_queue = nullptr;
int m_queueIndex = 0;
bool m_resumePlayback = true;
void setItem(Item *newItem);
void swapMediaPlayer();
bool m_qmlIsParsingComponent = false;
/**
@ -136,8 +196,13 @@ private:
/**
* @brief Retrieves the URL of the stream to open.
*/
void fetchStreamUrl();
void fetchStreamUrl(const Item *item, bool autoOpen, const FetchCallback &callback);
void fetchAndSetStreamUrl(const Item *item);
void setStreamUrl(const QString &streamUrl);
void setPlaybackState(QMediaPlayer::State newState);
Item *nextItem();
void setQueue(ItemModel *itemModel);
// Factor to multiply with when converting from milliseconds to ticks.
const static int MS_TICK_FACTOR = 10000;
@ -149,6 +214,7 @@ private:
*/
void postPlaybackInfo(PlaybackInfoType type);
void classBegin() override {
m_qmlIsParsingComponent = true;
}

View file

@ -25,7 +25,16 @@ namespace Jellyfin {
namespace DTO {
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
const QRegularExpression JsonSerializable::m_hashExpression = QRegularExpression("^QHash<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*,\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) { }
JsonSerializable::~JsonSerializable() {
if (parent() == nullptr) {
qDebug() << "Deleting" << metaObject()->className() << ", parent: nullptr, ownership: " << QQmlEngine::objectOwnership(this);
} else {
qDebug() << "Deleting" << metaObject()->className() << ", parent: " << parent()->metaObject()->className()
<< ", ownership: " << QQmlEngine::objectOwnership(this);
}
}
void JsonSerializable::deserialize(const QJsonObject &jObj) {
const QMetaObject *obj = this->metaObject();
@ -95,45 +104,55 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
JsonHelper::convertToCamelCase(QJsonValueRef(&tmp, 0));
return QVariant(innerObj);
} else {
return deserializeQobject(innerObj, prop);
return deserializeQObject(innerObj, prop);
}
}
return QVariant();
}
QVariant JsonSerializable::deserializeQobject(const QJsonObject &innerObj, const QMetaProperty &prop) {
QVariant JsonSerializable::deserializeQObject(const QJsonObject &innerObj, const QMetaProperty &prop) {
int typeNo = prop.userType();
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
if (metaType == nullptr) {
// Try to determine if the type is a qlist
QRegularExpressionMatch match = m_listExpression.match(prop.typeName());
if (match.hasMatch()) {
// It is a qList! Now extract the inner type
// There should be an easier way, shouldn't there?
QString listType = match.captured(1).prepend("Jellyfin::DTO::").append("*");
// UGLY CODE HERE WE COME
typeNo = QMetaType::type(listType.toUtf8());
if (typeNo == QMetaType::UnknownType) {
qDebug() << "Unknown type: " << listType;
return QVariant();
}
metaType = QMetaType::metaObjectForType(typeNo);
} else {
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
return QVariant();
}
}
QObject *deserializedInnerObj = metaType->newInstance();
deserializedInnerObj->setParent(this);
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
ser->deserialize(innerObj);
return QVariant(typeNo, &ser);
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
if (metaType == nullptr) {
// Try to determine if the type is a qlist
QRegularExpressionMatch listMatch = m_listExpression.match(prop.typeName());
QRegularExpressionMatch hashMatch = m_hashExpression.match(prop.typeName());
if (listMatch.hasMatch()) {
// It is a qList! Now extract the inner type
// There should be an easier way, shouldn't there?
QString listType = listMatch.captured(1);
typeNo = findTypeIdForProperty(listType);
metaType = QMetaType::metaObjectForType(typeNo);
} else {
deserializedInnerObj->deleteLater();
qDebug() << "Object is not a serializable one!";
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
return QVariant();
}
}
QObject *deserializedInnerObj = metaType->newInstance();
deserializedInnerObj->setParent(this);
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
// qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
ser->deserialize(innerObj);
return QVariant(typeNo, &ser);
} else {
deserializedInnerObj->deleteLater();
qDebug() << "Object is not a serializable one!";
return QVariant();
}
}
int JsonSerializable::findTypeIdForProperty(QString type) {
// UGLY CODE HERE WE COME
// We assume the type is either in no namespace (Qt Types) or in the Jellyfin::DTO namespace.
int typeNo = QMetaType::type(type.toUtf8());
if (typeNo == QMetaType::UnknownType) {
typeNo = QMetaType::type(type.prepend("Jellyfin::DTO::").append("*").toUtf8());
if (typeNo == QMetaType::UnknownType) {
qDebug() << "Unknown type: " << type;
return typeNo;
}
}
return typeNo;
}
QJsonObject JsonSerializable::serialize(bool capitalize) const {
@ -220,6 +239,13 @@ void RemoteData::setApiClient(ApiClient *newApiClient) {
reload();
}
void RemoteData::setExtraFields(const QStringList &extraFields) {
if (extraFields != m_extraFields) {
emit extraFieldsChanged(extraFields);
reload();
}
}
void RemoteData::reload() {
if (!canReload() || m_apiClient == nullptr) {
setStatus(Uninitialised);
@ -227,7 +253,11 @@ void RemoteData::reload() {
} else {
setStatus(Loading);
}
QNetworkReply *rep = m_apiClient->get(getDataUrl());
QUrlQuery params;
if (m_extraFields.length() > 0) {
params.addQueryItem("fields", m_extraFields.join(","));
}
QNetworkReply *rep = m_apiClient->get(getDataUrl() + QStringLiteral("?") + params.toString(QUrl::EncodeReserved));
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
rep->deleteLater();

View file

@ -43,7 +43,7 @@ QString Item::getDataUrl() const {
}
bool Item::canReload() const {
return !m_id.isNull();
return !m_id.isNull() && m_apiClient != nullptr;
}
void Item::setJellyfinId(QString newId) {
@ -54,7 +54,7 @@ void Item::setJellyfinId(QString newId) {
}
}
void Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
void Item::onUserDataChanged(const QString &itemId, UserData *userData) {
if (itemId != m_id || m_userData == nullptr) return;
m_userData->onUpdated(userData);
}

View file

@ -27,7 +27,7 @@ void UserData::updateOnServer() {
//TODO: implement
}
void UserData::onUpdated(QSharedPointer<UserData> other) {
void UserData::onUpdated(UserData *other) {
// The reason I'm not using setLikes and similar is that they don't work with std::nullopt,
// since QML does not like it.
// THe other reason is that the setLikes method will send a post request to the server, to update the contents

View file

@ -45,7 +45,7 @@ void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &p
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(version()));
QString url = this->m_baseUrl + path;
if (!params.isEmpty()) url += "?" + params.toString();
if (!params.isEmpty()) url += "?" + params.toString(QUrl::EncodeReserved);
request.setUrl(url);
}
@ -263,7 +263,7 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
rep->deleteLater();
}
void ApiClient::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
void ApiClient::onUserDataChanged(const QString &itemId, UserData *userData) {
emit userDataChanged(itemId, userData);
}

View file

@ -19,81 +19,70 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/DTO/item.h"
#include "JellyfinQt/DTO/userdata.h"
#include "JellyfinQt/DTO/user.h"
namespace Jellyfin {
ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel (parent),
// BaseApiModel
BaseApiModel::BaseApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel(parent),
m_path(path),
m_hasRecordResponse(hasRecordResponse),
m_addUserId(addUserId){
m_addUserId(addUserId) {
}
void ApiModel::reload() {
void BaseApiModel::setApiClient(ApiClient *apiClient) {
m_apiClient = apiClient;
emit apiClientChanged(m_apiClient);
}
void BaseApiModel::setLimit(int newLimit) {
m_limit = newLimit;
emit limitChanged(newLimit);
if (m_apiClient != nullptr && !m_isBeingParsed) {
load(LOAD_MORE);
}
}
void BaseApiModel::reload() {
this->setStatus(Loading);
m_startIndex = 0;
load(RELOAD);
}
void ApiModel::load(LoadType type) {
void BaseApiModel::load(LoadType type) {
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
if (m_apiClient == nullptr) {
qWarning() << "Please set the apiClient property before (re)loading";
return;
}
if (m_path.contains("{{user}}")) {
m_path = m_path.replace("{{user}}", m_apiClient->userId());
}
if (m_path.contains("{{show}}") && !m_show.isEmpty()) {
m_path = m_path.replace("{{show}}", m_show);
}
QString path(m_path);
replacePathPlaceholders(path);
QUrlQuery query;
if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
}
if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId);
}
if (!m_sortBy.empty()) {
query.addQueryItem("SortBy", m_sortBy.join(","));
}
if (m_sortOrder != Unspecified) {
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
}
if (!m_imageTypes.empty()) {
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
}
if (!m_includeItemTypes.empty()) {
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
}
if (!m_fields.empty()) {
query.addQueryItem("Fields", m_fields.join(","));
}
if (!m_seasonId.isEmpty()) {
query.addQueryItem("seasonId", m_seasonId);
}
if (m_addUserId) {
query.addQueryItem("userId", m_apiClient->userId());
}
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
addQueryParameters(query);
QNetworkReply *rep = m_apiClient->get(m_path, query);
QNetworkReply *rep = m_apiClient->get(path, query);
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
qDebug() << rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ": " << rep->request().url();
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
if (doc.isNull()) {
qWarning() << "JSON parse error";
this->setStatus(Error);
}
if (!m_hasRecordResponse) {
if (!doc.isArray()) {
qWarning() << "Object is not an array!";
this->setStatus(Error);
return;
}
this->m_array = doc.array();
QJsonArray items = doc.array();
setModelData(items);
} else {
if (!doc.isObject()) {
qWarning() << "Object is not an object!";
@ -125,42 +114,109 @@ void ApiModel::load(LoadType type) {
QJsonArray items = obj["Items"].toArray();
switch(type) {
case RELOAD:
this->m_array = items;
setModelData(items);
break;
case LOAD_MORE:
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
for (auto it = items.begin(); it != items.end(); it++) {
JsonHelper::convertToCamelCase(*it);
}
foreach (const QJsonValue &val, items) {
m_array.append(val);
}
this->endInsertRows();
appendModelData(items);
break;
}
}
if (type == RELOAD) {
generateFields();
}
this->setStatus(Ready);
rep->deleteLater();
});
}
void ApiModel::generateFields() {
if (m_array.size() == 0) return;
this->beginResetModel();
void BaseApiModel::addQueryParameters(QUrlQuery &query) {
if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
}
if (!m_sortBy.empty()) {
query.addQueryItem("SortBy", m_sortBy.join(","));
}
if (m_sortOrder != Unspecified) {
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
}
if (!m_fields.empty()) {
query.addQueryItem("Fields", m_fields.join(","));
}
if (m_addUserId) {
query.addQueryItem("userId", m_apiClient->userId());
}
}
void BaseApiModel::replacePathPlaceholders(QString &path) {
if (path.contains("{{user}}")) {
path = path.replace("{{user}}", m_apiClient->userId());
}
}
void BaseApiModel::classBegin() {
m_isBeingParsed = true;
}
void BaseApiModel::componentComplete() {
m_isBeingParsed = false;
}
// ApiModel
template <class T>
ApiModel<T>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
// If based on QObject, we know our role names before the first request
generateFields();
}
template <>
ApiModel<QJsonValue>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
// But we only know our role names after our first request.
}
template <class T>
T *ApiModel<T>::deserializeResult(QJsonValueRef source) {
T *result = new T(static_cast<BaseApiModel *>(this));
result->deserialize(source.toObject());
return result;
}
template <>
QJsonValue *ApiModel<QJsonValue>::deserializeResult(QJsonValueRef source) {
QJsonValue *result = new QJsonValue(source);
JsonHelper::convertToCamelCase(*result);
return result;
}
template <class T>
void ApiModel<T>::generateFields() {
const QMetaObject *obj = &T::staticMetaObject;
m_roles[Qt::UserRole + 1] = "qtObject";
for (int i = 0; i < obj->propertyCount(); i++) {
QMetaProperty property = obj->property(i);
m_roles.insert(Qt::UserRole + 2 + i, property.name());
}
}
template <>
void ApiModel<QJsonValue>::generateFields() {
// We can only generate field names if there is a first item. Redefining role names later leads to
// unexpected results, so prevent it as well.
if (m_array.size() == 0 || m_roles.size() > 0) return;
int i = Qt::UserRole + 1;
if (!m_array[0].isObject()) {
if (!m_array[0]->isObject()) {
qWarning() << "Iterator is not an object?";
return;
}
// Walks over the keys in the first record and adds them to the rolenames.
// This assumes the back-end has the same keys for every record. I could technically
// go over all records to be really sure, but no-one got time for a O(n²) algorithm, so
// go over all records to be really sure, but no-one got time for a O(n) algorithm, so
// this heuristic hopefully suffices.
QJsonObject ob = m_array[0].toObject();
QJsonObject ob = m_array[0]->toObject();
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
QString keyName = jt.key();
keyName[0] = keyName[0].toLower();
@ -169,20 +225,78 @@ void ApiModel::generateFields() {
m_roles.insert(i++, keyArr);
}
}
for (auto it = m_array.begin(); it != m_array.end(); it++){
JsonHelper::convertToCamelCase(*it);
}
template <class T>
void ApiModel<T>::setModelData(QJsonArray &data) {
this->beginResetModel();
for (T* value : m_array) {
value->deleteLater();
}
m_array.clear();
for(QJsonValueRef value : data) {
m_array.append(deserializeResult(value));
}
this->endResetModel();
}
QVariant ApiModel::data(const QModelIndex &index, int role) const {
template <>
void ApiModel<QJsonValue>::setModelData(QJsonArray &data) {
generateFields();
this->beginResetModel();
for (QJsonValue* value : m_array) {
delete value;
}
m_array.clear();
for(QJsonValueRef value : data) {
m_array.append(deserializeResult(value));
}
this->endResetModel();
}
template <class T>
void ApiModel<T>::appendModelData(QJsonArray &data) {
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + data.size() - 1);
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
for (auto it = data.begin(); it != data.end(); it++) {
JsonHelper::convertToCamelCase(*it);
}
for(QJsonValueRef val : data) {
m_array.append(deserializeResult(val));
}
this->endInsertRows();
}
template <class T>
QVariant ApiModel<T>::data(const QModelIndex &index, int role) const {
// Ignore roles we don't know
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
// Ignore invalid indices.
if (!index.isValid()) return QVariant();
QJsonObject obj = m_array.at(index.row()).toObject();
T* obj = m_array.at(index.row());
// m_roleNames[role] == "qtObject"
if (role == Qt::UserRole + 1) {
return QVariant::fromValue(obj);
}
const QString &key = m_roles[role];
if (role - Qt::UserRole - 2 < obj->metaObject()->propertyCount() ) {
return obj->property(key.toLocal8Bit());
}
return QVariant();
}
template <>
QVariant ApiModel<QJsonValue>::data(const QModelIndex &index, int role) const {
// Ignore roles we don't know
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
// Ignore invalid indices.
if (!index.isValid()) return QVariant();
QJsonObject obj = m_array.at(index.row())->toObject();
const QString &key = m_roles[role];
@ -192,7 +306,9 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
return QVariant();
}
bool ApiModel::canFetchMore(const QModelIndex &parent) const {
template <class T>
bool ApiModel<T>::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid()) return false;
switch(m_status) {
case Uninitialised:
@ -208,50 +324,99 @@ bool ApiModel::canFetchMore(const QModelIndex &parent) const {
} else {
return false;
}
}
void ApiModel::fetchMore(const QModelIndex &parent) {
template <class T>
void ApiModel<T>::fetchMore(const QModelIndex &parent) {
if (parent.isValid()) return;
this->setStatus(LoadingMore);
load(LOAD_MORE);
}
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
template <class T>
void ApiModel<T>::addQueryParameters(QUrlQuery &query) {
BaseApiModel::addQueryParameters(query);
}
template <class T>
void ApiModel<T>::replacePathPlaceholders(QString &path) {
BaseApiModel::replacePathPlaceholders(path);
}
template <class T>
void ApiModel<T>::insert(int index, T* object) {
Q_ASSERT(index >=0 && index <= size());
this->beginInsertRows(QModelIndex(), index, index);
m_array.insert(index, object);
this->endInsertRows();
}
template <class T>
void ApiModel<T>::removeAt(int index) {
this->beginRemoveRows(QModelIndex(), index, index);
m_array.removeAt(index);
this->endRemoveRows();
}
template <class T>
void ApiModel<T>::removeOne(T* object) {
int idx = m_array.indexOf(object);
if (idx >= 0) {
removeAt(idx);
}
}
// Itemmodel
ItemModel::ItemModel(QString path, bool hasRecordFields, bool replaceUser, QObject *parent)
: ApiModel (path, hasRecordFields, replaceUser, parent){
connect(this, &ApiModel::apiClientChanged, this, [this](ApiClient *newApiClient) {
connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
QObject::connect(this, &BaseApiModel::apiClientChanged, static_cast<BaseApiModel *>(this), [this](ApiClient *newApiClient) {
QObject::connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
});
}
void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer<DTO::UserData> userData) {
void ItemModel::onUserDataChanged(const QString &itemId, DTO::UserData *userData) {
int i = 0;
for (QJsonValueRef val: m_array) {
QJsonObject item = val.toObject();
if (item.contains("id") && item["id"].toString() == itemId) {
if (item.contains("userData")) {
QModelIndex cell = this->index(i);
item["userData"] = userData->serialize(false);
val = item;
this->dataChanged(cell, cell);
}
for (Item *val: m_array) {
if (val->userData() != nullptr && val->jellyfinId() == itemId) {
QModelIndex cell = this->index(i);
val->userData()->onUpdated(userData);
this->dataChanged(cell, cell);
}
i++;
}
}
void ItemModel::addQueryParameters(QUrlQuery &query) {
ApiModel<Item>::addQueryParameters(query);
if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId);
}
if (!m_imageTypes.empty()) {
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
}
if (!m_includeItemTypes.empty()) {
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
}
if (!m_seasonId.isEmpty()) {
query.addQueryItem("seasonId", m_seasonId);
}
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
}
void ItemModel::replacePathPlaceholders(QString &path) {
ApiModel::replacePathPlaceholders(path);
if (path.contains("{{show}}") && !m_show.isEmpty()) {
path = m_path.replace("{{show}}", m_show);
}
}
PublicUserModel::PublicUserModel(QObject *parent)
: ApiModel ("/users/public", false, false, parent) { }
UserViewModel::UserViewModel(QObject *parent)
: ApiModel ("/Users/{{user}}/Views", true, false, parent) {}
: ItemModel ("/Users/{{user}}/Views", true, false, parent) {}
UserItemModel::UserItemModel(QObject *parent)
: ItemModel ("/Users/{{user}}/Items", true, false, parent) {}
@ -272,7 +437,7 @@ ShowEpisodesModel::ShowEpisodesModel(QObject *parent)
: ItemModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
void registerModels(const char *URI) {
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<BaseApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");

View file

@ -51,6 +51,37 @@ void convertToCamelCase(QJsonValueRef val) {
break;
}
}
void convertToCamelCase(QJsonValue &val) {
switch(val.type()) {
case QJsonValue::Object: {
QJsonObject obj = val.toObject();
for(const QString &key: obj.keys()) {
QJsonValueRef ref = obj[key];
convertToCamelCase(ref);
obj[convertToCamelCaseHelper(key)] = ref;
if (key[0].isLower() || !key[0].isLetter()) {
obj[key] = ref;
} else {
obj[convertToCamelCaseHelper(key)] = ref;
obj.remove(key);
}
}
val = obj;
break;
}
case QJsonValue::Array: {
QJsonArray arr = val.toArray();
for (auto it = arr.begin(); it != arr.end(); it++) {
convertToCamelCase(*it);
}
val = arr;
break;
}
default:
break;
}
}
QString convertToCamelCaseHelper(const QString &str) {
QString res(str);

View file

@ -19,25 +19,29 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/playbackmanager.h"
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/DTO/dto.h"
#include "JellyfinQt/DTO/userdata.h"
namespace Jellyfin {
class ItemModel;
PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent) {
: QObject(parent), m_mediaPlayer1(new QMediaPlayer(this)), m_mediaPlayer2(new QMediaPlayer(this)) {
// Set up connections.
swapMediaPlayer();
m_updateTimer.setInterval(10000); // 10 seconds
m_updateTimer.setSingleShot(false);
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
}
void PlaybackManager::fetchStreamUrl() {
if (m_item == nullptr || m_apiClient == nullptr) {
void PlaybackManager::fetchStreamUrl(const Item *item, bool autoOpen, const FetchCallback &callback) {
if (item == nullptr || m_apiClient == nullptr) {
qDebug() << "Item or apiClient not set";
return;
}
m_resumePosition = 0;
if (m_resumePlayback && !m_item->property("userData").isNull()) {
if (m_resumePlayback && !item->property("userData").isNull()) {
UserData* userData = qvariant_cast<UserData *>(m_item->property("userData"));
if (userData != nullptr) {
m_resumePosition = userData->playbackPositionTicks();
@ -47,16 +51,16 @@ void PlaybackManager::fetchStreamUrl() {
params.addQueryItem("UserId", m_apiClient->userId());
params.addQueryItem("StartTimeTicks", QString::number(m_resumePosition));
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
params.addQueryItem("MediaSourceId", this->m_item->jellyfinId());
params.addQueryItem("AutoOpenLiveStream", autoOpen? "true" : "false");
params.addQueryItem("MediaSourceId", item->jellyfinId());
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
QNetworkReply *rep = m_apiClient->post("/Items/" + item->jellyfinId() + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep, callback]() {
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
this->m_playSessionId = root["PlaySessionId"].toString();
qDebug() << "Session id: " << this->m_playSessionId;
@ -81,25 +85,35 @@ void PlaybackManager::fetchStreamUrl() {
}
QString streamUrl = this->m_apiClient->baseUrl() + "/" + mediaType + "/" + m_item->jellyfinId() + "/stream."
+ firstMediaSource["Container"].toString() + "?" + query.toString(QUrl::EncodeReserved);
setStreamUrl(streamUrl);
this->m_playMethod = DirectPlay;
callback(QUrl(streamUrl), DirectPlay);
} else if (firstMediaSource["SupportsTranscoding"].toBool() && !firstMediaSource["TranscodingUrl"].isNull()) {
QString streamUrl = this->m_apiClient->baseUrl()
+ firstMediaSource["TranscodingUrl"].toString();
this->m_playMethod = Transcode;
setStreamUrl(streamUrl);
callback(QUrl(streamUrl), Transcode);
} else {
qDebug() << "No stream url found";
return;
}
qDebug() << "Found stream url: " << this->m_streamUrl;
}
rep->deleteLater();
});
}
void PlaybackManager::fetchAndSetStreamUrl(const Item *item) {
fetchStreamUrl(item, m_autoOpen, [this, item](QUrl &&url, PlayMethod playbackMethod) {
if (m_item == item) {
setStreamUrl(url.toString());
m_playMethod = playbackMethod;
emit playMethodChanged(m_playMethod);
m_mediaPlayer->setMedia(QMediaContent(url));
m_mediaPlayer->play();
}
});
}
void PlaybackManager::setItem(Item *newItem) {
if (m_mediaPlayer != nullptr) m_mediaPlayer->stop();
@ -109,8 +123,6 @@ void PlaybackManager::setItem(Item *newItem) {
}
this->m_item = newItem;
emit itemChanged(newItem);
// Don't try to start fetching when we're not completely parsed yet.
if (m_qmlIsParsingComponent) return;
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
@ -130,27 +142,33 @@ void PlaybackManager::setItem(Item *newItem) {
newItem->setParent(this);
}
if (m_item->status() == RemoteData::Ready) {
fetchStreamUrl();
fetchAndSetStreamUrl(m_item);
} else {
connect(m_item, &RemoteData::ready, [this]() -> void {
fetchStreamUrl();
fetchAndSetStreamUrl(m_item);
});
}
}
}
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
this->m_streamUrl = streamUrl;
// Inspired by PHP naming schemes
QUrl realStreamUrl(streamUrl);
Q_ASSERT_X(realStreamUrl.isValid(), "setStreamUrl", "StreamURL Jellyfin returned is not valid");
if (m_mediaPlayer != nullptr) {
m_mediaPlayer->setMedia(QMediaContent(realStreamUrl));
}
emit streamUrlChanged(streamUrl);
}
void PlaybackManager::setPlaybackState(QMediaPlayer::State newState) {
if (newState != m_playbackState) {
m_playbackState = newState;
emit playbackStateChanged(newState);
}
}
void PlaybackManager::mediaPlayerPositionChanged(qint64 position) {
emit positionChanged(position);
if (position == 0 && m_oldPosition != 0) {
// Save the old position when stop gets called. The QMediaPlayer will try to set
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
@ -171,6 +189,7 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
// We've stopped playing the media. Post a stop signal.
m_updateTimer.stop();
postPlaybackInfo(Stopped);
setPlaybackState(QMediaPlayer::StoppedState);
} else {
postPlaybackInfo(Progress);
}
@ -178,8 +197,10 @@ void PlaybackManager::mediaPlayerStateChanged(QMediaPlayer::State newState) {
}
void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus newStatus) {
emit mediaStatusChanged(newStatus);
if (newStatus == QMediaPlayer::LoadedMedia) {
m_mediaPlayer->play();
setPlaybackState(playbackState());
if (m_resumePlayback) {
qDebug() << "Resuming playback by seeking to " << (m_resumePosition / MS_TICK_FACTOR);
m_mediaPlayer->setPosition(m_resumePosition / MS_TICK_FACTOR);
@ -187,24 +208,9 @@ void PlaybackManager::mediaPlayerMediaStatusChanged(QMediaPlayer::MediaStatus ne
}
}
void PlaybackManager::setMediaPlayer(QObject *qmlMediaPlayer) {
if (m_mediaPlayer != nullptr) {
// Clean up the old media player.
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
}
m_qmlMediaPlayer = qmlMediaPlayer;
if (qmlMediaPlayer != nullptr) {
m_mediaPlayer = qvariant_cast<QMediaPlayer *>(qmlMediaPlayer->property("mediaObject"));
Q_ASSERT_X(m_mediaPlayer != nullptr, "setMediaPlayer", "The mediaPlayer property must contain a qml MediaPlayer with the mediaObject property");
// Connect signals from the new media player
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
}
void PlaybackManager::mediaPlayerError(QMediaPlayer::Error error) {
emit errorChanged(error);
emit errorStringChanged(m_mediaPlayer->errorString());
}
void PlaybackManager::updatePlaybackInfo() {
@ -213,17 +219,45 @@ void PlaybackManager::updatePlaybackInfo() {
void PlaybackManager::playItem(const QString &itemId) {
Item *newItem = new Item(itemId, m_apiClient, this);
QString parentId = newItem->parentId();
setItem(newItem);
ItemModel *queue = new UserItemModel(this);
setQueue(queue);
connect(newItem, &Item::ready, this, [this, queue, parentId](){
queue->setParentId(parentId);
queue->setLimit(10000);
queue->setApiClient(m_apiClient);
queue->reload();
});
connect(queue, &BaseApiModel::ready, this, [this, queue, newItem]() {
for (int i = 0; i < queue->size(); i++) {
if (queue->at(i)->jellyfinId() == newItem->jellyfinId()) {
m_queueIndex = i;
emit queueIndexChanged(m_queueIndex);
break;
}
}
});
setPlaybackState(QMediaPlayer::PlayingState);
}
void PlaybackManager::playItemInList(ItemModel *playlist, int itemIdx) {
playlist->setParent(this);
setQueue(playlist);
m_queueIndex = itemIdx;
emit queueIndexChanged(m_queueIndex);
setItem(playlist->at(itemIdx));
}
void PlaybackManager::next() {
Q_UNIMPLEMENTED();
Q_UNIMPLEMENTED();
}
void PlaybackManager::previous() {
Q_UNIMPLEMENTED();
Q_UNIMPLEMENTED();
}
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
QJsonObject root;
@ -272,15 +306,62 @@ void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
m_apiClient->setDefaultErrorHandler(rep);
}
void PlaybackManager::swapMediaPlayer() {
if (m_mediaPlayer != nullptr) {
disconnect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
disconnect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
disconnect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
}
if (m_mediaPlayer == m_mediaPlayer1) {
m_mediaPlayer = m_mediaPlayer2;
emit mediaPlayerChanged(m_mediaPlayer);
} else {
m_mediaPlayer = m_mediaPlayer1;
emit mediaPlayerChanged(m_mediaPlayer);
}
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, &PlaybackManager::durationChanged);
connect(m_mediaPlayer, &QMediaPlayer::mediaStatusChanged, this, &PlaybackManager::mediaPlayerMediaStatusChanged);
connect(m_mediaPlayer, &QMediaPlayer::videoAvailableChanged, this, &PlaybackManager::hasVideoChanged);
// I do not like the complicated overload cast
connect(m_mediaPlayer, SIGNAL(error(QMediaPlayer::error)), this, SLOT(mediaPlayerError(QmediaPlayer::error)));
}
Item *PlaybackManager::nextItem() {
if (m_queue == nullptr) return nullptr;
// TODO: shuffle etc.
if (m_queueIndex < m_queue->size()) {
return m_queue->at(m_queueIndex + 1);
}
return nullptr;
}
void PlaybackManager::setQueue(ItemModel *model) {
if (m_queue != nullptr) {
if (QQmlEngine::objectOwnership(m_queue) == QQmlEngine::CppOwnership) {
m_queue->deleteLater();
} else {
m_queue->setParent(nullptr);
}
}
m_queue = model;
emit queueChanged(m_queue);
}
void PlaybackManager::componentComplete() {
if (m_apiClient == nullptr) qWarning() << "No ApiClient set for PlaybackManager";
m_qmlIsParsingComponent = false;
if (m_item != nullptr) {
if (m_item->status() == RemoteData::Ready) {
fetchStreamUrl();
fetchAndSetStreamUrl(m_item);
} else {
connect(m_item, &RemoteData::ready, [this]() -> void {
fetchStreamUrl();
fetchAndSetStreamUrl(m_item);
});
}
}

View file

@ -105,9 +105,11 @@ void WebSocket::textMessageReceived(const QString &message) {
}
QJsonArray userDataList = data2["UserDataList"].toArray();
for (QJsonValue val: userDataList) {
QSharedPointer<DTO::UserData> userData(new DTO::UserData, &QObject::deleteLater);
UserData* userData =new DTO::UserData;
userData->deserialize(val.toObject());
userData->setParent(this);
m_apiClient->onUserDataChanged(userData->itemId(), userData);
userData->deleteLater();
}
}