diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index dfd9d36..7a4fdb5 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -14,7 +14,8 @@ TARGET = harbour-sailfin QT += multimedia websockets -CONFIG += sailfishapp c++11 +CONFIG += sailfishapp # c++17 +QMAKE_CXXFLAGS += -std=c++17 # Help, something keeps eating my quotes and backslashes @@ -30,6 +31,7 @@ SOURCES += \ src/jellyfinapiclient.cpp \ src/jellyfinapimodel.cpp \ src/jellyfindeviceprofile.cpp \ + src/jellyfinitem.cpp \ src/jellyfinmediasource.cpp \ src/jellyfinwebsocket.cpp \ src/serverdiscoverymodel.cpp @@ -90,6 +92,7 @@ HEADERS += \ src/jellyfinapiclient.h \ src/jellyfinapimodel.h \ src/jellyfindeviceprofile.h \ + src/jellyfinitem.h \ src/jellyfinmediasource.h \ src/jellyfinwebsocket.h \ src/serverdiscoverymodel.h diff --git a/qml/Utils.js b/qml/Utils.js index 993c000..229e81d 100644 --- a/qml/Utils.js +++ b/qml/Utils.js @@ -46,8 +46,8 @@ function ticksToText(ticks) { } function itemImageUrl(baseUrl, item, type, options) { - if (!item.ImageTags[type]) { return "" } - return itemModelImageUrl(baseUrl, item.Id, item.ImageTags[type], type, options) + if (!item.imageTags[type]) { return "" } + return itemModelImageUrl(baseUrl, item.jellyfinId, item.imageTags[type], type, options) } function itemModelImageUrl(baseUrl, itemId, tag, type, options) { diff --git a/qml/pages/itemdetails/BaseDetailPage.qml b/qml/pages/itemdetails/BaseDetailPage.qml index 2091181..174afe4 100644 --- a/qml/pages/itemdetails/BaseDetailPage.qml +++ b/qml/pages/itemdetails/BaseDetailPage.qml @@ -31,9 +31,11 @@ import "../../components" */ Page { id: pageRoot - property string itemId: "" - property var itemData: ({}) - property bool _loading: true + property alias itemId: jItem.jellyfinId + property alias itemData: jItem + //property string itemId: "" + //property var itemData: ({}) + property bool _loading: jItem.status === "Loading" readonly property bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined") readonly property var _backdropImages: itemData.BackdropImageTags readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags @@ -66,11 +68,11 @@ Page { running: pageRoot._loading } - onItemIdChanged: { - itemData = {} - if (itemId.length && PageStatus.Active) { - pageRoot._loading = true - ApiClient.fetchItem(itemId) + JellyfinItem { + id: jItem + apiClient: ApiClient + onStatusChanged: { + console.log("Status changed: " + newStatus, JSON.stringify(jItem)) } } @@ -81,7 +83,7 @@ Page { } if (status == PageStatus.Active) { if (itemId) { - ApiClient.fetchItem(itemId) + //ApiClient.fetchItem(itemId) } } @@ -92,7 +94,7 @@ Page { onItemFetched: { if (itemId === pageRoot.itemId) { //console.log(JSON.stringify(result)) - pageRoot.itemData = result + //pageRoot.itemData = result pageRoot._loading = false if (status == PageStatus.Active) { if (itemData.Type === "CollectionFolder") { diff --git a/qml/pages/itemdetails/EpisodePage.qml b/qml/pages/itemdetails/EpisodePage.qml index f05cbfc..e167140 100644 --- a/qml/pages/itemdetails/EpisodePage.qml +++ b/qml/pages/itemdetails/EpisodePage.qml @@ -26,12 +26,12 @@ import "../../" VideoPage { subtitle: { - if (typeof itemData.IndexNumberEnd !== "undefined") { - qsTr("Episode %1–%2 | %3").arg(itemData.IndexNumber) - .arg(itemData.IndexNumberEnd) - .arg(itemData.SeasonName) + if (typeof itemData.indexNumberEnd !== "undefined") { + qsTr("Episode %1–%2 | %3").arg(itemData.indexNumber) + .arg(itemData.indexNumberEnd) + .arg(itemData.seasonName) } else { - qsTr("Episode %1 | %2").arg(itemData.IndexNumber).arg(itemData.SeasonName) + qsTr("Episode %1 | %2").arg(itemData.indexNumber).arg(itemData.seasonName) } } @@ -41,7 +41,7 @@ VideoPage { PlainLabel { id: overviewText - text: itemData.Overview || qsTr("No overview available") + text: itemData.overview || qsTr("No overview available") font.pixelSize: Theme.fontSizeSmall color: Theme.secondaryHighlightColor } diff --git a/qml/pages/itemdetails/FilmPage.qml b/qml/pages/itemdetails/FilmPage.qml index ed7a3d1..5632fad 100644 --- a/qml/pages/itemdetails/FilmPage.qml +++ b/qml/pages/itemdetails/FilmPage.qml @@ -26,7 +26,7 @@ import "../../components" import "../.." VideoPage { - subtitle: qsTr("Released: %1 — Run time: %2").arg(itemData.ProductionYear).arg(Utils.ticksToText(itemData.RunTimeTicks)) + subtitle: qsTr("Released: %1 — Run time: %2").arg(itemData.productionYear).arg(Utils.ticksToText(itemData.runTimeTicks)) SectionHeader { text: qsTr("Overview") @@ -34,7 +34,7 @@ VideoPage { PlainLabel { id: overviewText - text: itemData.Overview + text: itemData.overview font.pixelSize: Theme.fontSizeSmall color: Theme.secondaryHighlightColor } diff --git a/qml/pages/itemdetails/SeriesPage.qml b/qml/pages/itemdetails/SeriesPage.qml index 8e6617c..3538986 100644 --- a/qml/pages/itemdetails/SeriesPage.qml +++ b/qml/pages/itemdetails/SeriesPage.qml @@ -35,7 +35,7 @@ BaseDetailPage { PageHeader { id: header - title: itemData.Name + title: itemData.name visible: !hasLogo } @@ -52,7 +52,7 @@ BaseDetailPage { PlainLabel { id: overviewText - text: itemData.Overview + text: itemData.overview font.pixelSize: Theme.fontSizeSmall color: Theme.secondaryHighlightColor } @@ -65,7 +65,7 @@ BaseDetailPage { ShowSeasonsModel { id: showSeasonsModel apiClient: ApiClient - show: itemData.Id + show: itemData.jellyfinId } SilicaListView { @@ -87,7 +87,7 @@ BaseDetailPage { } } onItemDataChanged: { - showSeasonsModel.show = itemData.Id + showSeasonsModel.show = itemData.jellyfinId showSeasonsModel.reload() } } diff --git a/qml/pages/itemdetails/UnsupportedPage.qml b/qml/pages/itemdetails/UnsupportedPage.qml index f0d70d7..aa0f3cf 100644 --- a/qml/pages/itemdetails/UnsupportedPage.qml +++ b/qml/pages/itemdetails/UnsupportedPage.qml @@ -24,12 +24,12 @@ BaseDetailPage { SilicaFlickable { anchors.fill: parent PageHeader { - title: itemData.Name + title: itemData.name } ViewPlaceholder { enabled: true - text: qsTr("Item type (%1) unsupported").arg(itemData.Type) + text: qsTr("Item type (%1) unsupported").arg(itemData.type) hintText: qsTr("This is still an alpha version :)") } } diff --git a/qml/pages/itemdetails/VideoPage.qml b/qml/pages/itemdetails/VideoPage.qml index f962aa6..17b7691 100644 --- a/qml/pages/itemdetails/VideoPage.qml +++ b/qml/pages/itemdetails/VideoPage.qml @@ -45,7 +45,7 @@ BaseDetailPage { PageHeader { id: pageHeader - title: itemData.Name + title: itemData.name description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks)) } @@ -66,7 +66,7 @@ BaseDetailPage { VideoTrackSelector { id: trackSelector width: parent.width - tracks: itemData.MediaStreams + tracks: itemData.mediaStreams } } } diff --git a/rpm/harbour-sailfin.changes.run b/rpm/harbour-sailfin.changes.run index 76c3761..37ee1b6 100644 --- a/rpm/harbour-sailfin.changes.run +++ b/rpm/harbour-sailfin.changes.run @@ -23,3 +23,4 @@ git-change-log # Use the subjects (first lines) of tag annotations when no entry would be # included for a revision otherwise #git-change-log --auto-add-annotations +exit 0 diff --git a/src/harbour-sailfin.cpp b/src/harbour-sailfin.cpp index 284eb4b..687c252 100644 --- a/src/harbour-sailfin.cpp +++ b/src/harbour-sailfin.cpp @@ -30,6 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "jellyfinapiclient.h" #include "jellyfinapimodel.h" +#include "jellyfinitem.h" #include "jellyfinmediasource.h" #include "serverdiscoverymodel.h" @@ -47,6 +48,7 @@ void registerQml() { // API models Jellyfin::registerModels(QML_NAMESPACE); + Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE); } int main(int argc, char *argv[]) { diff --git a/src/jellyfinitem.cpp b/src/jellyfinitem.cpp new file mode 100644 index 0000000..51ced56 --- /dev/null +++ b/src/jellyfinitem.cpp @@ -0,0 +1,201 @@ +#include "jellyfinitem.h" + +namespace Jellyfin { +JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) {} + +void JsonSerializable::deserialize(const QJsonObject &jObj) { + const QMetaObject *obj = this->metaObject(); + + // Loop over each property, + for (int i = 0; i < obj->propertyCount(); i++) { + QMetaProperty prop = obj->property(i); + // Skip properties which are not stored (usually derrived of other properties) + if (!prop.isStored()) continue; + if (!prop.isWritable()) continue; + + qDebug() << toPascalCase(prop.name()); + // Hardcoded exception for the property id, since its special inside QML + if (QString(prop.name()) == "jellyfinId" && jObj.contains("Id")) { + QJsonValue val = jObj["Id"]; + prop.write(this, jsonToVariant(prop, val, jObj)); + } else if (jObj.contains(toPascalCase(prop.name()))) { + QJsonValue val = jObj[toPascalCase(prop.name())]; + prop.write(this, jsonToVariant(prop, val, jObj)); + } else { + qDebug() << "Ignored " << prop.name() << " while deserializing"; + } + } +} + +QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) const { + switch(val.type()) { + case QJsonValue::Null: + case QJsonValue::Undefined: + return QVariant(); + case QJsonValue::Bool: + case QJsonValue::Double: + case QJsonValue::String: + return val.toVariant(); + case QJsonValue::Array: + if (prop.type() == QVariant::List) { + QJsonArray arr = val.toArray(); + QVariantList varArr; + for (auto it = arr.begin(); it < arr.end(); it++) { + varArr << jsonToVariant(prop, *it, root); + } + return QVariant(varArr); + } else { + qDebug() << prop.name() << " is not a " << prop.typeName(); + return QVariant(); + } + case QJsonValue::Object: + QJsonObject innerObj = val.toObject(); + QObject *deserializedInnerObj = QMetaType::metaObjectForType(prop.userType())->newInstance(); + if (JsonSerializable *ser = dynamic_cast(deserializedInnerObj)) { + qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className(); + ser->deserialize(innerObj); + return QVariant::fromValue(ser); + } else { + qDebug() << "Object is not a serializable one!"; + return QVariant(); + } + } + return QVariant(); +} + +QJsonObject JsonSerializable::serialize() const { + QJsonObject result; + const QMetaObject *obj = this->metaObject(); + for (int i = 0; i < obj->propertyCount(); i++) { + QMetaProperty prop = obj->property(i); + if (QString(prop.name()) == "jellyfinId") { + result["Id"] = variantToJson(prop.read(this)); + } else { + result[toPascalCase(prop.name())] = variantToJson(prop.read(this)); + } + } + return result; +} + +QJsonValue JsonSerializable::variantToJson(const QVariant var) const { + switch(var.type()) { + case QVariant::Invalid: + return QJsonValue(); + case QVariant::UserType: + if (var.canConvert()) { + JsonSerializable * obj = var.value(); + return obj->serialize(); + } else { + qWarning() << "Not serializable: " << var.typeName(); + return QJsonValue(); + } + case QVariant::Bool: + return var.toBool(); + case QVariant::List: + { + QVariantList list = var.toList(); + QJsonArray arr; + for (auto it = list.begin(); it < list.end(); it++) { + arr << variantToJson(*it); + } + return arr; + } + default: + if (var.canConvert(QVariant::Double)) { + return var.toDouble(); + } if (var.canConvert(QVariant::String)) { + return var.toString(); + } else { + return QJsonValue(); + } + } +} + +QString JsonSerializable::toPascalCase(QString str) { + str[0] = str[0].toUpper(); + return str; +} + +QString JsonSerializable::fromPascalCase(QString str) { + str[0] = str[0].toLower(); + return str; +} + + +// RemoteData +RemoteData::RemoteData(QObject *parent) : JsonSerializable (parent) {} + +void RemoteData::setStatus(Status newStatus) { + m_status = newStatus; + emit statusChanged(newStatus); +} + +void RemoteData::setError(QNetworkReply::NetworkError error) { + m_error = error; + emit errorChanged(error); +} + +void RemoteData::setErrorString(const QString &newErrorString) { + m_errorString = newErrorString; + emit errorStringChanged(newErrorString); +} + +void RemoteData::setApiClient(ApiClient *newApiClient) { + m_apiClient = newApiClient; + emit apiClientChanged(newApiClient); + reload(); +} + +// Item + +Item::Item(QObject *parent) : RemoteData(parent) { +} + + +void Item::setJellyfinId(QString newId) { + m_id = newId.trimmed(); + if (m_id != newId) { + emit jellyfinIdChanged(m_id); + reload(); + } +} + +void Item::reload() { + if (m_id.isEmpty() || m_apiClient == nullptr) { + setStatus(Uninitialised); + return; + } else { + setStatus(Loading); + } + QNetworkReply *rep = m_apiClient->get("/Users/" + m_apiClient->userId() + "/Items/" + m_id); + connect(rep, &QNetworkReply::finished, this, [this, rep]() { + rep->deleteLater(); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(rep->readAll(), &error); + if (doc.isNull()) { + this->setError(QNetworkReply::ProtocolFailure); + this->setErrorString(error.errorString()); + return; + } + if (!doc.isObject()) { + this->setError(QNetworkReply::ProtocolFailure); + this->setErrorString(tr("Invalid response from the server: root element is not an object.")); + return; + } + this->deserialize(doc.object()); + this->setStatus(Ready); + }); + connect(rep, static_cast(&QNetworkReply::error), + this, [this, rep](QNetworkReply::NetworkError error) { + rep->deleteLater(); + this->setError(error); + this->setErrorString(rep->errorString()); + this->setStatus(Error); + }); +} + +void registerSerializableJsonTypes(const char* URI) { + qmlRegisterType(URI, 1, 0, "JellyfinItem"); +} +} diff --git a/src/jellyfinitem.h b/src/jellyfinitem.h new file mode 100644 index 0000000..fb58587 --- /dev/null +++ b/src/jellyfinitem.h @@ -0,0 +1,249 @@ +#ifndef JELLYFINITEM_H +#define JELLYFINITEM_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include "jellyfinapiclient.h" + +namespace Jellyfin { + +/** + * @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() const; +private: + QVariant jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) const; + QJsonValue variantToJson(const QVariant var) const; + + /** + * @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 str); +}; + +/** + * @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); +public slots: + virtual void reload() = 0; +protected: + 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 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 + + // Handpicked, important ones + 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 setProductionYear NOTIFY indexNumberChanged) + + 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); } + + // Handpicked, important ones + int productionYear() const { return m_productionYear.value_or(-1); } + void setProductionYear(int newProductionYear) { m_productionYear = newProductionYear; emit productionYearChanged(newProductionYear); } + int indexNumber() const { return m_indexNumber.value_or(-1); } + void setIndexNumber(int newIndexNumber) { m_indexNumber = newIndexNumber; emit indexNumberChanged(newIndexNumber); } + +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); + + // Handpicked, important ones + void overviewChanged(const QString &newOverview); + void productionYearChanged(int newProductionYear); + void indexNumberChanged(int newIndexNumber); + +public slots: + /** + * @brief (Re)loads the item from the Jellyfin server. + */ + void reload() override; +protected: + 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 m_airsBeforeSeasonNumber = std::nullopt; + std::optional m_airsAfterSeasonNumber = std::nullopt; + std::optional m_airsBeforeEpisodeNumber = std::nullopt; + std::optional m_canDelete = std::nullopt; + std::optional m_canDownload = std::nullopt; + std::optional m_hasSubtitles = std::nullopt; + QString m_preferredMetadataLanguage; + QString m_preferredMetadataCountryCode; + std::optional m_supportsSync = std::nullopt; + QString m_container; + QString m_sortName; + QString m_forcedSortName; + QDateTime m_premiereDate; + + // Handpicked, important ones + QString m_overview; + std::optional m_productionYear = std::nullopt; + std::optional m_indexNumber = std::nullopt; +}; + +void registerSerializableJsonTypes(const char* URI); +} + +#endif // JELLYFINITEM_H diff --git a/translations/harbour-sailfin.ts b/translations/harbour-sailfin.ts index 86461c8..729b55e 100644 --- a/translations/harbour-sailfin.ts +++ b/translations/harbour-sailfin.ts @@ -141,6 +141,13 @@ + + Jellyfin::Item + + Invalid response from the server: root element is not an object. + + + LegalPage @@ -218,6 +225,10 @@ Retry + + Refresh + + SeasonPage