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

Switched to a sane build system (CMake)

This commit is contained in:
Chris Josten 2020-10-25 19:58:02 +01:00
parent 9cfd6d7ee2
commit 228bcfb685
36 changed files with 286 additions and 211 deletions

View file

@ -0,0 +1,130 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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 CREDENTIALS_MANAGER_H
#define CREDENTIALS_MANAGER_H
#include <QDebug>
#include <QHash>
#include <QObject>
#include <QSettings>
#include <QString>
/**
* @brief The CredentialsManager class stores credentials for users.
*
* You can get an instance using ::instance(), which may depend on the platform being
* used. Since the implementation may be asynchronous, the methods won't return anything,
* they emit a corresponding signal instead.
*/
class CredentialsManager : public QObject {
Q_OBJECT
public:
/**
* @brief Stores a token
* @param server The server to store the token for
* @param user The user to store the token for.
* @param token The token to store.
*/
virtual void store(const QString &server, const QString &user, const QString &token) {
Q_UNUSED(server)
Q_UNUSED(user)
Q_UNUSED(token)
Q_UNIMPLEMENTED();
}
/**
* @brief Retrieves a stored token. Emits tokenRetrieved when the token is retrieved.
* @param server The serverId to retrieve the token from.
* @param user The user to retrieve the token for
*/
virtual void get(const QString &server, const QString &user) const {
Q_UNUSED(server)
Q_UNUSED(user)
Q_UNIMPLEMENTED();
}
/**
* @brief removes a token
* @param server
* @param user
*/
virtual void remove(const QString &server, const QString &user) {
Q_UNUSED(server)
Q_UNUSED(user)
Q_UNIMPLEMENTED();
}
/**
* @brief Gives the list of servers that have a user stored with a token.
*/
virtual void listServers() const { Q_UNIMPLEMENTED(); }
/**
* @brief List the users with a token on a server
* @param server
*/
virtual void listUsers(const QString &server) {
Q_UNUSED(server)
Q_UNIMPLEMENTED();
}
/**
* @brief Retrieves an implementation which can store this token.
* @param The parent to set the implementations QObject parent to
*
* This method is always guaranteed to return an instance.
* @return An implementation of this interface (may vary acrros platform).
*/
static CredentialsManager *newInstance(QObject *parent = nullptr);
/**
* @return if the implementation of this interface stores the token in a secure place.
*/
virtual bool isSecure() const { return false; }
signals:
void tokenRetrieved(const QString &server, const QString &user, const QString &token) const;
void serversListed(const QStringList &servers) const;
void usersListed(const QStringList &users) const;
protected:
explicit CredentialsManager(QObject *parent = nullptr) : QObject (parent) {}
};
/**
* @brief Implementation of CredentialsManager that stores credentials in plain-text
*/
class FallbackCredentialsManager : public CredentialsManager {
Q_OBJECT
public:
FallbackCredentialsManager(QObject *parent = nullptr);
void store(const QString &server, const QString &user, const QString &token) override;
void get(const QString &server, const QString &user) const override;
void remove(const QString &server, const QString &user) override;
void listServers() const override;
void listUsers(const QString &server) override;
bool isSecure() const override { return false; }
private:
QString urlToGroupName(const QString &url) const;
QString groupNameToUrl(const QString &group) const;
QSettings m_settings;
};
#endif // CREDENTIAL_MANAGER_H

View file

@ -0,0 +1,16 @@
#ifndef JELLYFIN_H
#define JELLYFIN_H
#include <QtQml>
#include "JellyfinQt/jellyfinapiclient.h"
#include "JellyfinQt/jellyfinapimodel.h"
#include "JellyfinQt/jellyfinitem.h"
#include "JellyfinQt/serverdiscoverymodel.h"
#include "JellyfinQt/jellyfinplaybackmanager.h"
namespace Jellyfin {
void registerTypes();
}
#endif // JELLYFIN_H

View file

@ -0,0 +1,267 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_API_CLIENT
#define JELLYFIN_API_CLIENT
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QHostInfo>
#include <QObject>
#include <QString>
#include <QSysInfo>
#include <QtQml>
#include <QUuid>
#include <QNetworkReply>
#include <QUrlQuery>
#include "credentialmanager.h"
#include "jellyfindeviceprofile.h"
#include "jellyfinitem.h"
#include "jellyfinwebsocket.h"
namespace Jellyfin {
class MediaSource;
class WebSocket;
/**
* @brief An Api client for Jellyfin. Handles requests and authentication.
*
* This class should also be given to certain models and other sources, so they are able to make
* requests to the correct server.
*
* General usage is as follows:
* 1. (Optional) Call restoreSavedSession(). This will try to load previously saved credentials and connect to the server.
* If all succeeds, the property authenticated should be set to true and its signal should be emitted. All is done.
* If it fails, setupRequired will be emitted. Continue following these steps.
* 2. If opting in to manually manage the session or restoreSavedSession() failed, you'll need to set the property
* baseUrl to the root of the Jellyfin server, e.g. "https://jellyfin.example.com:8098", so not the url to the
* web interface! Nearby servers can be discovered using Jellyfin::ServerDiscoveryModel.
* 3. Call ::setupConnection(). First of all, the client will try to resolve any redirects and will update
* the baseUrl property if following redirects. Then it will emit connectionSuccess(QString). The QString from
* the signal contains a user-oriented login message configured by the user that should be displayed in the URL
* somewhere.
* 4. After ::connected is emitted, call ::authenticate(QString, QString, bool). with the username and password.
* The last boolean argument is used if you want to have the ApiClient store your credentials, so that they
* later can be used with restoreSavedSession().
* 5. If the authenticated property is set to true, you are now authenticated! If loginError() is emitted, you aren't and
* you should go back to step 4.
*
* These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up.
*/
class ApiClient : public QObject {
friend class WebSocket;
Q_OBJECT
public:
explicit ApiClient(QObject *parent = nullptr);
Q_PROPERTY(QString baseUrl MEMBER m_baseUrl READ baseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged)
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
Q_PROPERTY(QString version READ version)
/*QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination,
QVariantMap filters, QStringList fields, QStringList expand, QString id);*/
bool authenticated() const { return m_authenticated; }
void setBaseUrl(QString url) {
this->m_baseUrl = url;
if (this->m_baseUrl.endsWith("/")) {
this->m_baseUrl.chop(1);
}
emit this->baseUrlChanged(m_baseUrl);
}
QNetworkReply *get(const QString &path, const QUrlQuery &params = QUrlQuery());
QNetworkReply *post(const QString &path, const QJsonDocument &data = QJsonDocument(), const QUrlQuery &params = QUrlQuery());
void getPublicUsers();
enum ApiError {
JSON_ERROR,
UNEXPECTED_REPLY,
UNEXPECTED_STATUS,
INVALID_PASSWORD
};
QString &baseUrl() { return this->m_baseUrl; }
QString &userId() { return m_userId; }
QJsonObject &deviceProfile() { return m_deviceProfile; }
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
QString version() const;
/**
* @brief Sets the error handler of a reply to this classes default error handler
* @param rep The reply to set the error handler on.
*
* Motivation for this helper is because I forget the correct signature each time, with all the
* funky casts.
*/
void setDefaultErrorHandler(QNetworkReply *rep) {
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, &ApiClient::defaultNetworkErrorHandler);
}
signals:
/*
* Emitted when the server requires authentication. Please authenticate your user via authenticate.
*/
void authenticationRequired();
void authenticationError(ApiError error);
void connectionFailed(ApiError error);
void connectionSuccess(QString loginMessage);
void networkError(QNetworkReply::NetworkError error);
void authenticatedChanged(bool authenticated);
void baseUrlChanged(const QString &baseUrl);
/**
* @brief Set-up is required. You'll need to manually set up the baseUrl-property, call setupConnection
* afterwards and finally call authenticate.
*/
void setupRequired();
void userIdChanged(QString userId);
void itemFetched(const QString &itemId, const QJsonObject &result);
void itemFetchFailed(const QString &itemId, const QNetworkReply::NetworkError error);
/**
* @brief onUserDataChanged Emitted when the user data of an item is changed on the server.
* @param itemId The id of the item being changed
* @param userData The new user data
*
* Note: only Jellyfin::UserData should connect to this signal, they will update themselves!
*/
void userDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
public slots:
/**
* @brief Tries to access credentials and connect to a server. If nothing has been configured yet,
* emits setupRequired();
*/
void restoreSavedSession();
/*
* Try to connect with the server. Tries to resolve redirects and retrieves information
* about the login procedure. Emits connectionSuccess on success, networkError or ConnectionFailed
* otherwise.
*/
void setupConnection();
void authenticate(QString username, QString password, bool storeCredentials = false);
/**
* @brief Logs the user out and clears the session.
*/
void deleteSession();
void fetchItem(const QString &id);
/**
* @brief Shares the capabilities of this device to the server.
*/
void postCapabilities();
QString downloadUrl(const QString &itemId) const;
protected slots:
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> newData);
protected:
/**
* @brief Adds default headers to each request, like authentication headers etc.
* @param request The request to add headers to
* @param path The path to which the request is being made
*/
void addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery &params = QUrlQuery());
/**
* @brief Adds the authorization to the header
* @param The request to add the header to
*/
void addTokenHeader(QNetworkRequest &request);
/**
* @brief getBrandingConfiguration Gets the login message and custom CSS (which we ignore)
*/
void getBrandingConfiguration();
/**
* @brief Generates a profile, containing the name of the application, manufacturer and most importantly,
* which media types this device supports.
*
* The actual detection of supported media types is done within jellyfindeviceprofile.cpp, since the code
* is a big mess and should be safely contained in it's own file.
*/
void generateDeviceProfile();
QString &token() { return m_token; }
private:
QNetworkAccessManager m_naManager;
/*
* State information
*/
WebSocket *m_webSocket;
CredentialsManager * m_credManager;
QString m_token;
QString m_deviceName;
QString m_deviceId;
QString m_userId = "";
QJsonObject m_deviceProfile;
QJsonObject m_playbackDeviceProfile;
bool m_authenticated = false;
/**
* @brief The base url of the request.
*/
QString m_baseUrl;
/*
* Setters
*/
void setAuthenticated(bool authenticated);
void setUserId(QString userId) {
this->m_userId = userId;
emit userIdChanged(userId);
}
/*
* Utilities
*/
/**
* @brief Returns the statusCode of a QNetworkReply
* @param The reply to obtain the statusCode of
* @return The statuscode of the reply
*
* Seriously, Qt, why is your method to obtain the status code of a request so horrendous?
*/
static inline int statusCode(QNetworkReply *rep) {
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
};
} // NS Jellyfin
#endif // JELLYFIN_API_CLIENT

View file

@ -0,0 +1,309 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_API_MODEL
#define JELLYFIN_API_MODEL
#include <QAbstractListModel>
#include <QFlags>
#include <QMetaEnum>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtQml>
#include <QVariant>
#include "jellyfinapiclient.h"
#include "jsonhelper.h"
namespace Jellyfin {
class SortOptions : public QObject{
Q_OBJECT
public:
explicit SortOptions (QObject *parent = nullptr) : QObject(parent) {}
enum SortBy {
Album,
AlbumArtist,
Artist,
Budget,
CommunityRating,
CriticRating,
DateCreated,
DatePlayed,
PlayCount,
PremiereDate,
ProductionYear,
SortName,
Random,
Revenue,
Runtime
};
Q_ENUM(SortBy)
};
/**
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
* first record.
*
* To create a new model, extend this class and create an QObject-parent constructor.
* Call the right super constructor with the right values, depending which path should be queried and
* how the result should be interpreted.
*
* Register the model in QML and create an instance. Don't forget to set the apiClient attribute or else
* the model you've created will be useless!
*
* Rolenames are based on the fields in the first object within the array of results, with the first letter
* lowercased, to accomodate for QML style guidelines. (This ain't C# here).
*
* If a call to /cats/new results in
* @code{.json}
* [
* {"Name": "meow", "Id": 432},
* {"Name": "miew", "Id": 323}
* ]
* @endcode
* The model will have roleNames for "name" and "id".
*
*/
class ApiModel : public QAbstractListModel {
Q_OBJECT
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.
* @param subfield Leave empty if the root of the result is the array with results. Otherwise, set to the key name in the
* root object which contains the data.
* @param parent Parent (Standard QObject stuff)
*
* If the response looks something like this:
* @code{.json}
* [{...}, {...}, {...}]
* @endcode
*
* or
* @code{.json}
* {...}
* @endcode
* responseHasRecords should be false
*
* If the response looks something like this:
* @code{.json}
* {
* "Offset": 0,
* "Count": 20,
* "Items": [{...}, {...}, {...}, ..., {...}]
* }
* @endcode
* 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(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 {
if (!index.isValid()) return m_array.size();
return 0;
}
QHash<int, QByteArray> roleNames() const override { return m_roles; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
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(); }
template<typename QEnum>
QString enumListToString (const QList<QEnum> enumList) {
QString result;
for (QEnum e : enumList) {
result += QVariant::fromValue(e).toString() + ",";
}
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);
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 = {};
SortOrder m_sortOrder = Unspecified;
bool m_recursive = false;
QHash<int, QByteArray> m_roles;
void setStatus(ModelStatus newStatus) {
this->m_status = newStatus;
emit this->statusChanged(newStatus);
}
private:
/**
* @brief Generates roleNames based on the first record in m_array.
*/
void generateFields();
QString sortByToString(SortOptions::SortBy sortBy);
};
/**
* @brief List of the public users on the server.
*/
class PublicUserModel : public ApiModel {
public:
explicit PublicUserModel (QObject *parent = nullptr);
};
/**
* @brief Base class for each model that works with items.
*
* Listens for updates in the library and updates the model accordingly.
*/
class ItemModel : public ApiModel {
Q_OBJECT
public:
explicit ItemModel (QString path, bool responseHasRecords, bool replaceUser, QObject *parent = nullptr);
public slots:
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
};
class UserViewModel : public ApiModel {
public:
explicit UserViewModel (QObject *parent = nullptr);
};
class UserItemModel : public ItemModel {
public:
explicit UserItemModel (QObject *parent = nullptr);
};
class UserItemResumeModel : public ItemModel {
public:
explicit UserItemResumeModel (QObject *parent = nullptr);
};
class UserItemLatestModel : public ItemModel {
public:
explicit UserItemLatestModel (QObject *parent = nullptr);
};
class ShowNextUpModel : public ItemModel {
public:
explicit ShowNextUpModel (QObject *parent = nullptr);
};
class ShowSeasonsModel : public ItemModel {
public:
explicit ShowSeasonsModel (QObject *parent = nullptr);
};
class ShowEpisodesModel : public ItemModel {
public:
explicit ShowEpisodesModel (QObject *parent = nullptr);
};
void registerModels(const char *URI);
}
#endif //JELLYFIN_API_MODEL

View file

@ -0,0 +1,52 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_DEVICE_PROFILE_H
#define JELLYFIN_DEVICE_PROFILE_H
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <QMap>
#include <QString>
#include <QSysInfo>
#include <QtMultimedia/QMediaPlayer>
namespace Jellyfin {
namespace DeviceProfile {
QJsonObject generateProfile();
// Transport
bool supportsHls();
// Bitrate
int maxStreamingBitrate();
// Video codecs
bool canPlayH264();
bool canPlayH265();
// Audio codecs
bool canPlayAc3();
bool supportsMp3VideoAudio();
}
}
#endif // JELLYFIN_DEVICE_PROFILE_H

View file

@ -0,0 +1,492 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_ITEM_H
#define JELLYFIN_ITEM_H
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QDebug>
#include <QMetaObject>
#include <QMetaProperty>
#include <QDateTime>
#include <QObject>
#include <QRegularExpression>
#include <QtQml>
#include <QNetworkReply>
#include <optional>
#include <cmath>
#include "jellyfinapiclient.h"
#include "jsonhelper.h"
namespace Jellyfin {
class ApiClient;
/**
* @brief Base class for a serializable object.
*
* This class will be (de)serialized based on its properties.
* Note: it must have a constructor without arguments marked with Q_INVOKABLE
*/
class JsonSerializable : public QObject {
Q_OBJECT
public:
Q_INVOKABLE JsonSerializable(QObject *parent);
/**
* @brief Sets this objects properties based on obj.
* @param obj The data to load into this object.
*/
void deserialize(const QJsonObject &obj);
QJsonObject serialize(bool capitalize = true) const;
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);
/**
* @brief Sets the first letter of the string to lower case (to make it camelCase).
* @param str The string to modify
* @return THe modified string
*/
static QString fromPascalCase(QString str);
/**
* @brief Sets the first letter of the string to uper case (to make it PascalCase).
* @param str The string to modify
* @return THe modified string
*/
static QString toPascalCase(QString st);
static const QRegularExpression m_listExpression;
/**
* @brief Qt is doing weird. I'll keep track of the metatypes myself.
*/
QHash<QString, const QMetaType *> m_nameMetatypeMap;
};
/**
* @brief An "interface" for a remote data source
*
* This class is basically a base class for JSON data that can be fetched from over the network.
* Subclasses should reimplement reload and call setStatus to update the QML part of the code
* appropiatly.
*/
class RemoteData : public JsonSerializable {
Q_OBJECT
public:
enum Status {
/// The data is unitialized and not loading either.
Uninitialised,
/// The data is being loaded over the network
Loading,
/// The data is ready, the properties in this object are up to date.
Ready,
/// An error has occurred while loading the data. See error() for more details.
Error
};
Q_ENUM(Status)
explicit RemoteData(QObject *parent = nullptr);
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient WRITE setApiClient NOTIFY apiClientChanged STORED false)
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)
Status status() const { return m_status; }
QNetworkReply::NetworkError error() const { return m_error; }
QString errorString() const { return m_errorString; }
void setApiClient(ApiClient *newApiClient);
signals:
void statusChanged(Status newStatus);
void apiClientChanged(ApiClient *newApiClient);
void errorChanged(QNetworkReply::NetworkError newError);
void errorStringChanged(QString newErrorString);
/**
* @brief Convenience signal for status == RemoteData.Ready.
*/
void ready();
public slots:
/**
* @brief Overload this method to reimplement the fetching mechanism to
* populate the RemoteData with data from the server.
*
* The default implementation makes a GET request to getDataUrl() and parses the resulting JSON,
* which should be enough for most cases. Consider overriding getDataUrl() and
* canRelaod() if possible. Manual overrides need to make sure that
* they're calling setStatus(Status), setError(QNetworkReply::NetworkError) and
* setErrorString() to let the QML side know what this thing is up to.
*/
virtual void reload();
protected:
/**
* @brief Subclasses should implement this to determine if they can
* load data from the server.
*
* Usage cases include checking if the
* required properties, such as the item id are set.
*/
virtual bool canReload() const = 0;
/**
* @brief Construct the URL to fetch the data from.
* @return The URL to load data from.
*/
virtual QString getDataUrl() const = 0;
void setStatus(Status newStatus);
void setError(QNetworkReply::NetworkError error);
void setErrorString(const QString &newErrorString);
ApiClient *m_apiClient = nullptr;
private:
Status m_status = Uninitialised;
QNetworkReply::NetworkError m_error = QNetworkReply::NoError;
QString m_errorString;
};
class User : public RemoteData {
Q_OBJECT
public:
Q_INVOKABLE User(QObject *parent = nullptr);
Q_PROPERTY(QString userId MEMBER m_userId WRITE setUserId NOTIFY userIdChanged)
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
Q_PROPERTY(QString primaryImageTag MEMBER m_primaryImageTag NOTIFY primaryImageTagChanged)
void setUserId(const QString &newUserId) {
this->m_userId = newUserId;
emit userIdChanged(newUserId);
reload();
}
signals:
void userIdChanged(const QString &newUserId);
void nameChanged(const QString &newName);
void primaryImageTagChanged(const QString &newPrimaryImageTag);
protected:
QString getDataUrl() const override;
bool canReload() const override;
private:
QString m_userId;
QString m_name;
QString m_primaryImageTag;
};
class MediaStream : public JsonSerializable {
Q_OBJECT
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,
Audio,
Video,
Subtitle,
EmbeddedImage
};
Q_ENUM(MediaStreamType)
Q_PROPERTY(QString codec MEMBER m_codec NOTIFY codecChanged)
Q_PROPERTY(QString codecTag MEMBER m_codecTag NOTIFY codecTagChanged)
Q_PROPERTY(QString language MEMBER m_language NOTIFY languageChanged)
Q_PROPERTY(QString displayTitle MEMBER m_displayTitle NOTIFY displayTitleChanged)
Q_PROPERTY(MediaStreamType type MEMBER m_type NOTIFY typeChanged)
Q_PROPERTY(int index MEMBER m_index NOTIFY indexChanged)
signals:
void codecChanged(const QString &newCodec);
void codecTagChanged(const QString &newCodecTag);
void languageChanged(const QString &newLanguage);
void displayTitleChanged(const QString &newDisplayTitle);
void typeChanged(MediaStreamType newType);
void indexChanged(int newIndex);
private:
QString m_codec;
QString m_codecTag;
QString m_language;
QString m_displayTitle;
MediaStreamType m_type = Undefined;
int m_index = -1;
};
class UserData : public JsonSerializable {
Q_OBJECT
public:
Q_INVOKABLE explicit UserData(QObject *parent = nullptr);
Q_PROPERTY(double playedPercentage READ playedPercentage WRITE setPlayedPercentage RESET resetPlayedPercentage NOTIFY playedPercentageChanged)
Q_PROPERTY(qint64 playbackPositionTicks READ playbackPositionTicks WRITE setPlaybackPositionTicks NOTIFY playbackPositionTicksChanged)
Q_PROPERTY(bool isFavorite READ isFavorite WRITE setIsFavorite NOTIFY isFavoriteChanged)
Q_PROPERTY(bool likes READ likes WRITE setLikes RESET resetLikes NOTIFY likesChanged)
Q_PROPERTY(bool played READ played WRITE setPlayed NOTIFY playedChanged)
Q_PROPERTY(QString itemId READ itemId MEMBER m_itemId);
double playedPercentage() const { return m_playedPercentage.value_or(0.0); }
void setPlayedPercentage(double newPlayedPercentage) { m_playedPercentage = newPlayedPercentage; emit playedPercentageChanged(newPlayedPercentage); }
void resetPlayedPercentage() { m_playedPercentage = std::nullopt; emit playedPercentageChanged(0.0); updateOnServer(); }
qint64 playbackPositionTicks() const { return m_playbackPositionTicks; }
void setPlaybackPositionTicks(qint64 newPlaybackPositionTicks) { m_playbackPositionTicks = newPlaybackPositionTicks; emit playbackPositionTicksChanged(newPlaybackPositionTicks); }
bool played() const { return m_played; }
void setPlayed(bool newPlayed) { m_played = newPlayed; emit playedChanged(newPlayed); updateOnServer(); }
bool likes() const { return m_likes.value_or(false); }
void setLikes(bool newLikes) { m_likes = newLikes; emit likesChanged(newLikes); }
void resetLikes() { m_likes = std::nullopt; emit likesChanged(false); updateOnServer(); }
bool isFavorite() const { return m_isFavorite; }
void setIsFavorite(bool newIsFavorite) { m_isFavorite = newIsFavorite; emit isFavoriteChanged(newIsFavorite); updateOnServer(); }
const QString &itemId() const { return m_itemId; }
signals:
void playedPercentageChanged(double newPlayedPercentage);
void playbackPositionTicksChanged(qint64 playbackPositionTicks);
void isFavoriteChanged(bool newIsFavorite);
void likesChanged(bool newLikes);
void playedChanged(bool newPlayed);
public slots:
void updateOnServer();
void onUpdated(QSharedPointer<UserData> other);
private:
std::optional<double> m_playedPercentage = std::nullopt;
qint64 m_playbackPositionTicks = 0;
bool m_isFavorite = false;
std::optional<bool> m_likes = std::nullopt;
bool m_played;
QString m_itemId;
};
class Item : public RemoteData {
Q_OBJECT
public:
Q_INVOKABLE explicit Item(QObject *parent = nullptr);
Q_PROPERTY(QString jellyfinId READ jellyfinId WRITE setJellyfinId NOTIFY jellyfinIdChanged)
// Based on https://github.com/jellyfin/jellyfin/blob/907695dec7fda152d0e17c1197637bc0e17c9928/MediaBrowser.Model/Dto/BaseItemDto.cs
// I copy, pasted and replaced. I feel like a Go programmer implementing generic containers.
// If this were D, I would've writed a compile-time C# parser to parse that source code at compile time, extract
// the properties and generate a class based on that.
// Doing that in C++ would be more difficult and I dislike qmake. Does it even support running programs at compile time?
// But here I am, using ctrl-C++
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
Q_PROPERTY(QString originalTitle MEMBER m_originalTitle NOTIFY originalTitleChanged)
Q_PROPERTY(QString serverId MEMBER m_serverId NOTIFY serverIdChanged)
Q_PROPERTY(QString etag MEMBER m_etag NOTIFY etagChanged)
Q_PROPERTY(QString sourceType MEMBER m_sourceType NOTIFY sourceTypeChanged)
Q_PROPERTY(QString playlistItemId MEMBER m_playlistItemId NOTIFY playlistItemIdChanged)
Q_PROPERTY(QDateTime dateCreated MEMBER m_dateCreated NOTIFY dateCreatedChanged)
Q_PROPERTY(QDateTime dateLastMediaAdded MEMBER m_dateLastMediaAdded NOTIFY dateLastMediaAddedChanged)
Q_PROPERTY(QString extraType MEMBER m_extraType NOTIFY extraTypeChanged)
Q_PROPERTY(int airsBeforeSeasonNumber READ airsBeforeSeasonNumber WRITE setAirsBeforeSeasonNumber NOTIFY airsBeforeSeasonNumberChanged)
Q_PROPERTY(int airsAfterSeasonNumber READ airsAfterSeasonNumber WRITE setAirsAfterSeasonNumber NOTIFY airsAfterSeasonNumberChanged)
Q_PROPERTY(int airsBeforeEpisodeNumber READ airsBeforeEpisodeNumber WRITE setAirsBeforeEpisodeNumber NOTIFY airsBeforeEpisodeNumberChanged)
Q_PROPERTY(bool canDelete READ canDelete WRITE setCanDelete NOTIFY canDeleteChanged)
Q_PROPERTY(bool canDownload READ canDownload WRITE setCanDownload NOTIFY canDownloadChanged)
Q_PROPERTY(bool hasSubtitles READ hasSubtitles WRITE setHasSubtitles NOTIFY hasSubtitlesChanged)
Q_PROPERTY(QString preferredMetadataLanguage MEMBER m_preferredMetadataLanguage NOTIFY preferredMetadataLanguageChanged)
Q_PROPERTY(QString preferredMetadataCountryCode MEMBER m_preferredMetadataCountryCode NOTIFY preferredMetadataCountryCodeChanged)
Q_PROPERTY(bool supportsSync READ supportsSync WRITE setSupportsSync NOTIFY supportsSyncChanged)
Q_PROPERTY(QString container MEMBER m_container NOTIFY containerChanged)
Q_PROPERTY(QString sortName MEMBER m_sortName NOTIFY sortNameChanged)
Q_PROPERTY(QString forcedSortName MEMBER m_forcedSortName NOTIFY forcedSortNameChanged)
//SKIP: Video3DFormat
Q_PROPERTY(QDateTime premiereData MEMBER m_premiereDate NOTIFY premiereDateChanged)
//SKIP: ExternalUrls
//SKIP: MediaSources
Q_PROPERTY(float criticRating READ criticRating WRITE setCriticRating NOTIFY criticRatingChanged)
Q_PROPERTY(QStringList productionLocations MEMBER m_productionLocations NOTIFY productionLocationsChanged)
// Handpicked, important ones
Q_PROPERTY(qint64 runTimeTicks READ runTimeTicks WRITE setRunTimeTicks NOTIFY runTimeTicksChanged)
Q_PROPERTY(QString overview MEMBER m_overview NOTIFY overviewChanged)
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(bool isFolder READ isFolder WRITE setIsFolder NOTIFY isFolderChanged)
Q_PROPERTY(QString type MEMBER m_type NOTIFY typeChanged)
Q_PROPERTY(UserData *userData MEMBER m_userData NOTIFY userDataChanged)
Q_PROPERTY(QString seriesName MEMBER m_seriesName NOTIFY seriesNameChanged)
Q_PROPERTY(QString seasonName MEMBER m_seasonName NOTIFY seasonNameChanged)
Q_PROPERTY(QList<MediaStream *> __list__mediaStreams MEMBER __list__m_mediaStreams NOTIFY mediaStreamsChanged)
Q_PROPERTY(QVariantList mediaStreams MEMBER m_mediaStreams NOTIFY mediaStreamsChanged STORED false)
// Why is this a QJsonObject? Well, because I couldn't be bothered to implement the deserialisations of
// a QHash at the moment.
Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged)
Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged)
QString jellyfinId() const { return m_id; }
void setJellyfinId(QString newId);
int airsBeforeSeasonNumber() const { return m_airsBeforeSeasonNumber.value_or(-1); }
void setAirsBeforeSeasonNumber(int newAirsBeforeSeasonNumber) { m_airsBeforeSeasonNumber = newAirsBeforeSeasonNumber; emit airsBeforeSeasonNumberChanged(newAirsBeforeSeasonNumber); }
int airsAfterSeasonNumber() const { return m_airsAfterSeasonNumber.value_or(-1); }
void setAirsAfterSeasonNumber(int newAirsAfterSeasonNumber) { m_airsAfterSeasonNumber = newAirsAfterSeasonNumber; emit airsAfterSeasonNumberChanged(newAirsAfterSeasonNumber); }
int airsBeforeEpisodeNumber() const { return m_airsBeforeEpisodeNumber.value_or(-1); }
void setAirsBeforeEpisodeNumber(int newAirsBeforeEpisodeNumber) { m_airsBeforeEpisodeNumber = newAirsBeforeEpisodeNumber; emit airsBeforeEpisodeNumberChanged(newAirsBeforeEpisodeNumber); }
bool canDelete() const { return m_canDelete.value_or(false); }
void setCanDelete(bool newCanDelete) { m_canDelete = newCanDelete; emit canDeleteChanged(newCanDelete); }
bool canDownload() const { return m_canDownload.value_or(false); }
void setCanDownload(bool newCanDownload) { m_canDownload = newCanDownload; emit canDownloadChanged(newCanDownload); }
bool hasSubtitles() const { return m_hasSubtitles.value_or(false); }
void setHasSubtitles(bool newHasSubtitles) { m_hasSubtitles = newHasSubtitles; emit hasSubtitlesChanged(newHasSubtitles); }
bool supportsSync() const { return m_supportsSync.value_or(false); }
void setSupportsSync(bool newSupportsSync) { m_supportsSync = newSupportsSync; emit supportsSyncChanged(newSupportsSync); }
float criticRating() const { return m_criticRating.value_or(std::nanf("")); }
void setCriticRating(float newCriticRating) { m_criticRating = newCriticRating; emit criticRatingChanged(newCriticRating); }
// Handpicked, important ones
qint64 runTimeTicks() const { return m_runTimeTicks.value_or(-1); }
void setRunTimeTicks(qint64 newRunTimeTicks) { m_runTimeTicks = newRunTimeTicks; emit runTimeTicksChanged(newRunTimeTicks); }
int productionYear() const { return m_productionYear.value_or(-1); }
void setProductionYear(int newProductionYear) { m_productionYear = std::optional<int>(newProductionYear); emit productionYearChanged(newProductionYear); }
int indexNumber() const { return m_indexNumber.value_or(-1); }
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); }
bool isFolder() const { return m_isFolder.value_or(false); }
void setIsFolder(bool newIsFolder) { m_isFolder = newIsFolder; emit isFolderChanged(newIsFolder); }
//QQmlListProperty<MediaStream> mediaStreams() { return toReadOnlyQmlListProperty<MediaStream>(m_mediaStreams); }
//QList<QObject *> mediaStreams() { return *reinterpret_cast<QList<QObject *> *>(&m_mediaStreams); }
QVariantList mediaStreams() { QVariantList l; for (auto e: m_mediaStreams) l.append(QVariant::fromValue(e)); return l;}
signals:
void jellyfinIdChanged(const QString &newId);
void nameChanged(const QString &newName);
void originalTitleChanged(const QString &newOriginalTitle);
void serverIdChanged(const QString &newServerId);
void etagChanged(const QString &newEtag);
void sourceTypeChanged(const QString &sourceType);
void playlistItemIdChanged(const QString &playlistItemIdChanged);
void dateCreatedChanged(QDateTime newDateCreatedChanged);
void dateLastMediaAddedChanged(QDateTime newDateLastMediaAdded);
void extraTypeChanged(const QString &newExtraType);
void airsBeforeSeasonNumberChanged(int newAirsBeforeSeasonNumber);
void airsAfterSeasonNumberChanged(int newAirsAfterSeasonNumber);
void airsBeforeEpisodeNumberChanged(int newAirsAfterEpisodeNumber);
bool canDeleteChanged(bool newCanDelete);
void canDownloadChanged(bool newCanDownload);
void hasSubtitlesChanged(bool newHasSubtitles);
void preferredMetadataLanguageChanged(const QString &newPreferredMetadataLanguage);
void preferredMetadataCountryCodeChanged(const QString &newPreferredMetadataCountryCode);
void supportsSyncChanged(bool newSupportsSync);
void containerChanged(const QString &newContainer);
void sortNameChanged(const QString &newSortName);
void forcedSortNameChanged(const QString &newForcedSortName);
void premiereDateChanged(QDateTime newPremiereDate);
void criticRatingChanged(float newCriticRating);
void productionLocationsChanged(QStringList newProductionLocations);
// Handpicked, important ones
void runTimeTicksChanged(qint64 newRunTimeTicks);
void overviewChanged(const QString &newOverview);
void productionYearChanged(int newProductionYear);
void indexNumberChanged(int newIndexNumber);
void indexNumberEndChanged(int newIndexNumberEnd);
void isFolderChanged(bool newIsFolder);
void typeChanged(const QString &newType);
void userDataChanged(UserData *newUserData);
void seriesNameChanged(const QString &newSeriesName);
void seasonNameChanged(const QString &newSeasonName);
void mediaStreamsChanged(/*const QList<MediaStream *> &newMediaStreams*/);
void imageTagsChanged();
void imageBlurHashesChanged();
public slots:
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
protected:
// Overrides
QString getDataUrl() const override;
bool canReload() const override;
QString m_id;
QString m_name;
QString m_originalTitle;
QString m_serverId;
QString m_etag;
QString m_sourceType;
QString m_playlistItemId;
QDateTime m_dateCreated;
QDateTime m_dateLastMediaAdded;
QString m_extraType;
std::optional<int> m_airsBeforeSeasonNumber = std::nullopt;
std::optional<int> m_airsAfterSeasonNumber = std::nullopt;
std::optional<int> m_airsBeforeEpisodeNumber = std::nullopt;
std::optional<bool> m_canDelete = std::nullopt;
std::optional<bool> m_canDownload = std::nullopt;
std::optional<bool> m_hasSubtitles = std::nullopt;
QString m_preferredMetadataLanguage;
QString m_preferredMetadataCountryCode;
std::optional<bool> m_supportsSync = std::nullopt;
QString m_container;
QString m_sortName;
QString m_forcedSortName;
QDateTime m_premiereDate;
std::optional<float> m_criticRating = std::nullopt;
QStringList m_productionLocations;
// Handpicked, important ones
std::optional<qint64> m_runTimeTicks = std::nullopt;
QString m_overview;
std::optional<int> m_productionYear = std::nullopt;
std::optional<int> m_indexNumber = std::nullopt;
std::optional<int> m_indexNumberEnd = std::nullopt;
std::optional<bool> m_isFolder = std::nullopt;
QString m_type;
UserData *m_userData = nullptr;
QString m_seriesName;
QString m_seasonName;
QList<MediaStream *> __list__m_mediaStreams;
QVariantList m_mediaStreams;
QJsonObject m_imageTags;
QJsonObject m_imageBlurHashes;
template<typename T>
QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) {
return QQmlListProperty<T>(this, std::addressof(list), &qlist_count, &qlist_at);
}
template<typename T>
static int qlist_count(QQmlListProperty<T> *p) {
return reinterpret_cast<QList<T *> *>(p->data)->count();
}
template<typename T>
static T *qlist_at(QQmlListProperty<T> *p, int idx) {
return reinterpret_cast<QList<T *> *>(p->data)->at(idx);
}
};
void registerSerializableJsonTypes(const char* URI);
}
#endif // JELLYFIN_ITEM_H

View file

@ -0,0 +1,112 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_MEDIA_SOURCE_H
#define JELLYFIN_MEDIA_SOURCE_H
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QVariant>
#include <QUrlQuery>
#include <QtMultimedia/QMediaPlayer>
#include "jellyfinapiclient.h"
namespace Jellyfin {
class PlaybackManager : public QObject {
Q_OBJECT
public:
enum PlayMethod {
Transcode,
Stream,
DirectPlay
};
Q_ENUM(PlayMethod)
explicit PlaybackManager(QObject *parent = nullptr);
Q_PROPERTY(ApiClient *apiClient MEMBER m_apiClient)
Q_PROPERTY(QString itemId READ itemId WRITE setItemId NOTIFY itemIdChanged)
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(qint64 position MEMBER m_position WRITE setPosition NOTIFY positionChanged)
Q_PROPERTY(QMediaPlayer::State state READ state WRITE setState NOTIFY stateChanged)
QString itemId() const { return m_itemId; }
void setItemId(const QString &newItemId);
QMediaPlayer::State state() const { return m_state; }
void setState(QMediaPlayer::State newState);
void setPosition(qint64 position);
QString streamUrl() const { return m_streamUrl; }
signals:
void itemIdChanged(const QString &newItemId);
void streamUrlChanged(const QString &newStreamUrl);
void autoOpenChanged(bool autoOpen);
void audioIndexChanged(int audioIndex);
void subtitleIndexChanged(int subtitleIndex);
void positionChanged(qint64 position);
void stateChanged(QMediaPlayer::State state);
public slots:
void updatePlaybackInfo();
private:
QTimer m_updateTimer;
ApiClient *m_apiClient = nullptr;
QString m_itemId;
QString m_streamUrl;
QString m_playSessionId;
int m_audioIndex = 0;
int m_subtitleIndex = -1;
qint64 m_position = 0;
qint64 m_stopPosition = 0;
PlayMethod m_playMethod;
QMediaPlayer::State m_state = QMediaPlayer::StoppedState;
/**
* @brief Whether to automatically open the livestream of the item;
*/
bool m_autoOpen = false;
void fetchStreamUrl();
void setStreamUrl(const QString &streamUrl);
// Factor to multiply with when converting from milliseconds to ticks.
const int MS_TICK_FACTOR = 10000;
enum PlaybackInfoType { Started, Stopped, Progress };
/**
* @brief Posts the playback information
*/
void postPlaybackInfo(PlaybackInfoType type);
};
}
#endif // JELLYFIN_MEDIA_SOURCE_H

View file

@ -0,0 +1,86 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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_WEBSOCKET_H
#define JELLYFIN_WEBSOCKET_H
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QDebug>
#include <QObject>
#include <QtGlobal>
#include <QTimer>
#include <QUuid>
#include <QtWebSockets/QWebSocket>
#include "jellyfinapiclient.h"
#include "jellyfinitem.h"
namespace Jellyfin {
class ApiClient;
class UserData;
/**
* @brief Keeps a connection with the Jellyfin server to receive real time updates.
*
* This class will parse these messages and send them to ApiClient, which will emit signals for
* the interested classes.
*/
class WebSocket : public QObject {
Q_OBJECT
public:
/**
* @brief WebSocket creates a webSocket for a Jellyfin server to handle real time updates.
* @param client The client to create the socket for.
*
* The socket will automatically set the ApiClient to its parent.
*/
explicit WebSocket(ApiClient *client);
enum MessageType {
ForceKeepAlive,
KeepAlive,
UserDataChanged
};
Q_ENUM(MessageType)
public slots:
void open();
private slots:
void textMessageReceived(const QString &message);
void onConnected();
void onDisconnected();
void sendKeepAlive();
signals:
void commandReceived(QString arts, QVariantMap args);
protected:
ApiClient *m_apiClient;
QWebSocket m_webSocket;
QTimer m_keepAliveTimer;
void setupKeepAlive(int data);
void sendMessage(MessageType type, QJsonValue data = QJsonValue());
QString generateMessageId();
};
}
#endif // JELLYFIN_WEBSOCKET_H

View file

@ -0,0 +1,21 @@
#ifndef JSON_SERIALIZER_H
#define JSON_SERIALIZER_H
#include <QList>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonValueRef>
#include <QString>
namespace Jellyfin {
namespace JsonHelper {
void convertToCamelCase(QJsonValueRef val);
QString convertToCamelCaseHelper(const QString &str);
};
}
#endif // JSONSERIALIZER_H

View file

@ -0,0 +1,83 @@
/*
Sailfin: a Jellyfin client written using Qt
Copyright (C) 2020 Chris Josten
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 SERVER_DISCOVERY_MODEL_H
#define SERVER_DISCOVERY_MODEL_H
#include <vector>
#include <QAbstractListModel>
#include <QHash>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QHostAddress>
#include <QUdpSocket>
namespace Jellyfin {
struct ServerDiscovery {
QString name;
QString address;
QString id;
};
/**
* @brief Discovers nearby Jellyfin servers and puts them in this list.
*/
class ServerDiscoveryModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
ROLE_NAME = Qt::UserRole + 1,
ROLE_ADDRESS,
ROLE_ID
};
explicit ServerDiscoveryModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override {
return {
{ROLE_NAME, "name"},
{ROLE_ADDRESS, "address"},
{ROLE_ID, "id"}
};
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
if (parent.isValid()) return 0;
return static_cast<int>(m_discoveredServers.size());
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
/**
* @brief Refreshes the model and searches for new servers
*/
void refresh();
private slots:
void on_datagramsAvailable();
private:
const QByteArray MAGIC_PACKET = "who is JellyfinServer?";
const quint16 BROADCAST_PORT = 7359;
std::vector<ServerDiscovery> m_discoveredServers;
QUdpSocket m_socket;
};
}
#endif //SERVER_DISCOVERY_MODEL_H