/* Sailfin: a Jellyfin client written using Qt Copyright (C) 2021 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 #include #include #include #include #include #include #include #include #include "apiclient.h" #include "jsonhelper.h" #include "dto/baseitemdto.h" #include "dto/userdto.h" #include "dto/useritemdatadto.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) }; /** * 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 sortBy MEMBER m_sortBy NOTIFY sortByChanged) Q_PROPERTY(QList 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 virtual int rowCount(const QModelIndex &index) const override = 0; virtual QHash 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 newSortOrder); void sortOrderChanged(SortOrder newSortOrder); void fieldsChanged(QList 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; // Query/record controlling properties int m_limit = -1; int m_startIndex = 0; int m_totalRecordCount = 0; const int DEFAULT_LIMIT = 100; // Query properties QList m_fields = {}; QList 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 * 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". * */ template class ApiModel : public BaseApiModel { public: /** * @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); // Standard QAbstractItemModel overrides int rowCount(const QModelIndex &index) const override { if (!index.isValid()) return m_array.size(); return 0; } QHash 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; // Helper methods template 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 QString enumListToString (const QList enumList) { QString result; for (QEnum e : enumList) { result += QVariant::fromValue(e).toString() + ","; } return result; } protected: // AbstractItemModel bookkeeping QHash m_roles; // 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 m_array; 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); // 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 imageTypes MEMBER m_imageTypes NOTIFY imageTypesChanged) Q_PROPERTY(QList includeItemTypes MEMBER m_includeItemTypes NOTIFY includeItemTypesChanged) Q_PROPERTY(bool recursive MEMBER m_recursive) QList 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 newImageTypes); void includeItemTypesChanged(const QList &newIncludeItemTypes); // Path property signals void showChanged(QString newShow); public slots: 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 m_imageTypes = {}; bool m_recursive = false; }; //template<> //void ApiModel::apiClientChanged(); class UserViewModel : public ItemModel { 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