mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 01:05:17 +00:00
Initial step towards the QObject rewrite
This commit is contained in:
parent
7c7d2ba195
commit
4e3395c4e5
|
@ -14,7 +14,8 @@ TARGET = harbour-sailfin
|
||||||
|
|
||||||
QT += multimedia websockets
|
QT += multimedia websockets
|
||||||
|
|
||||||
CONFIG += sailfishapp c++11
|
CONFIG += sailfishapp # c++17
|
||||||
|
QMAKE_CXXFLAGS += -std=c++17
|
||||||
|
|
||||||
# Help, something keeps eating my quotes and backslashes
|
# Help, something keeps eating my quotes and backslashes
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ SOURCES += \
|
||||||
src/jellyfinapiclient.cpp \
|
src/jellyfinapiclient.cpp \
|
||||||
src/jellyfinapimodel.cpp \
|
src/jellyfinapimodel.cpp \
|
||||||
src/jellyfindeviceprofile.cpp \
|
src/jellyfindeviceprofile.cpp \
|
||||||
|
src/jellyfinitem.cpp \
|
||||||
src/jellyfinmediasource.cpp \
|
src/jellyfinmediasource.cpp \
|
||||||
src/jellyfinwebsocket.cpp \
|
src/jellyfinwebsocket.cpp \
|
||||||
src/serverdiscoverymodel.cpp
|
src/serverdiscoverymodel.cpp
|
||||||
|
@ -90,6 +92,7 @@ HEADERS += \
|
||||||
src/jellyfinapiclient.h \
|
src/jellyfinapiclient.h \
|
||||||
src/jellyfinapimodel.h \
|
src/jellyfinapimodel.h \
|
||||||
src/jellyfindeviceprofile.h \
|
src/jellyfindeviceprofile.h \
|
||||||
|
src/jellyfinitem.h \
|
||||||
src/jellyfinmediasource.h \
|
src/jellyfinmediasource.h \
|
||||||
src/jellyfinwebsocket.h \
|
src/jellyfinwebsocket.h \
|
||||||
src/serverdiscoverymodel.h
|
src/serverdiscoverymodel.h
|
||||||
|
|
|
@ -46,8 +46,8 @@ function ticksToText(ticks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemImageUrl(baseUrl, item, type, options) {
|
function itemImageUrl(baseUrl, item, type, options) {
|
||||||
if (!item.ImageTags[type]) { return "" }
|
if (!item.imageTags[type]) { return "" }
|
||||||
return itemModelImageUrl(baseUrl, item.Id, item.ImageTags[type], type, options)
|
return itemModelImageUrl(baseUrl, item.jellyfinId, item.imageTags[type], type, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemModelImageUrl(baseUrl, itemId, tag, type, options) {
|
function itemModelImageUrl(baseUrl, itemId, tag, type, options) {
|
||||||
|
|
|
@ -31,9 +31,11 @@ import "../../components"
|
||||||
*/
|
*/
|
||||||
Page {
|
Page {
|
||||||
id: pageRoot
|
id: pageRoot
|
||||||
property string itemId: ""
|
property alias itemId: jItem.jellyfinId
|
||||||
property var itemData: ({})
|
property alias itemData: jItem
|
||||||
property bool _loading: true
|
//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 bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined")
|
||||||
readonly property var _backdropImages: itemData.BackdropImageTags
|
readonly property var _backdropImages: itemData.BackdropImageTags
|
||||||
readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags
|
readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags
|
||||||
|
@ -66,11 +68,11 @@ Page {
|
||||||
running: pageRoot._loading
|
running: pageRoot._loading
|
||||||
}
|
}
|
||||||
|
|
||||||
onItemIdChanged: {
|
JellyfinItem {
|
||||||
itemData = {}
|
id: jItem
|
||||||
if (itemId.length && PageStatus.Active) {
|
apiClient: ApiClient
|
||||||
pageRoot._loading = true
|
onStatusChanged: {
|
||||||
ApiClient.fetchItem(itemId)
|
console.log("Status changed: " + newStatus, JSON.stringify(jItem))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +83,7 @@ Page {
|
||||||
}
|
}
|
||||||
if (status == PageStatus.Active) {
|
if (status == PageStatus.Active) {
|
||||||
if (itemId) {
|
if (itemId) {
|
||||||
ApiClient.fetchItem(itemId)
|
//ApiClient.fetchItem(itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -92,7 +94,7 @@ Page {
|
||||||
onItemFetched: {
|
onItemFetched: {
|
||||||
if (itemId === pageRoot.itemId) {
|
if (itemId === pageRoot.itemId) {
|
||||||
//console.log(JSON.stringify(result))
|
//console.log(JSON.stringify(result))
|
||||||
pageRoot.itemData = result
|
//pageRoot.itemData = result
|
||||||
pageRoot._loading = false
|
pageRoot._loading = false
|
||||||
if (status == PageStatus.Active) {
|
if (status == PageStatus.Active) {
|
||||||
if (itemData.Type === "CollectionFolder") {
|
if (itemData.Type === "CollectionFolder") {
|
||||||
|
|
|
@ -26,12 +26,12 @@ import "../../"
|
||||||
|
|
||||||
VideoPage {
|
VideoPage {
|
||||||
subtitle: {
|
subtitle: {
|
||||||
if (typeof itemData.IndexNumberEnd !== "undefined") {
|
if (typeof itemData.indexNumberEnd !== "undefined") {
|
||||||
qsTr("Episode %1–%2 | %3").arg(itemData.IndexNumber)
|
qsTr("Episode %1–%2 | %3").arg(itemData.indexNumber)
|
||||||
.arg(itemData.IndexNumberEnd)
|
.arg(itemData.indexNumberEnd)
|
||||||
.arg(itemData.SeasonName)
|
.arg(itemData.seasonName)
|
||||||
} else {
|
} 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 {
|
PlainLabel {
|
||||||
id: overviewText
|
id: overviewText
|
||||||
text: itemData.Overview || qsTr("No overview available")
|
text: itemData.overview || qsTr("No overview available")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.secondaryHighlightColor
|
color: Theme.secondaryHighlightColor
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import "../../components"
|
||||||
import "../.."
|
import "../.."
|
||||||
|
|
||||||
VideoPage {
|
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 {
|
SectionHeader {
|
||||||
text: qsTr("Overview")
|
text: qsTr("Overview")
|
||||||
|
@ -34,7 +34,7 @@ VideoPage {
|
||||||
|
|
||||||
PlainLabel {
|
PlainLabel {
|
||||||
id: overviewText
|
id: overviewText
|
||||||
text: itemData.Overview
|
text: itemData.overview
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.secondaryHighlightColor
|
color: Theme.secondaryHighlightColor
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ BaseDetailPage {
|
||||||
|
|
||||||
PageHeader {
|
PageHeader {
|
||||||
id: header
|
id: header
|
||||||
title: itemData.Name
|
title: itemData.name
|
||||||
visible: !hasLogo
|
visible: !hasLogo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ BaseDetailPage {
|
||||||
|
|
||||||
PlainLabel {
|
PlainLabel {
|
||||||
id: overviewText
|
id: overviewText
|
||||||
text: itemData.Overview
|
text: itemData.overview
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.secondaryHighlightColor
|
color: Theme.secondaryHighlightColor
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ BaseDetailPage {
|
||||||
ShowSeasonsModel {
|
ShowSeasonsModel {
|
||||||
id: showSeasonsModel
|
id: showSeasonsModel
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
show: itemData.Id
|
show: itemData.jellyfinId
|
||||||
}
|
}
|
||||||
|
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
|
@ -87,7 +87,7 @@ BaseDetailPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onItemDataChanged: {
|
onItemDataChanged: {
|
||||||
showSeasonsModel.show = itemData.Id
|
showSeasonsModel.show = itemData.jellyfinId
|
||||||
showSeasonsModel.reload()
|
showSeasonsModel.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,12 @@ BaseDetailPage {
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
PageHeader {
|
PageHeader {
|
||||||
title: itemData.Name
|
title: itemData.name
|
||||||
}
|
}
|
||||||
ViewPlaceholder {
|
ViewPlaceholder {
|
||||||
|
|
||||||
enabled: true
|
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 :)")
|
hintText: qsTr("This is still an alpha version :)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ BaseDetailPage {
|
||||||
|
|
||||||
PageHeader {
|
PageHeader {
|
||||||
id: pageHeader
|
id: pageHeader
|
||||||
title: itemData.Name
|
title: itemData.name
|
||||||
description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks))
|
description: qsTr("Run time: %2").arg(Utils.ticksToText(itemData.RunTimeTicks))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ BaseDetailPage {
|
||||||
VideoTrackSelector {
|
VideoTrackSelector {
|
||||||
id: trackSelector
|
id: trackSelector
|
||||||
width: parent.width
|
width: parent.width
|
||||||
tracks: itemData.MediaStreams
|
tracks: itemData.mediaStreams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,4 @@ git-change-log
|
||||||
# Use the subjects (first lines) of tag annotations when no entry would be
|
# Use the subjects (first lines) of tag annotations when no entry would be
|
||||||
# included for a revision otherwise
|
# included for a revision otherwise
|
||||||
#git-change-log --auto-add-annotations
|
#git-change-log --auto-add-annotations
|
||||||
|
exit 0
|
||||||
|
|
|
@ -30,6 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
#include "jellyfinapiclient.h"
|
#include "jellyfinapiclient.h"
|
||||||
#include "jellyfinapimodel.h"
|
#include "jellyfinapimodel.h"
|
||||||
|
#include "jellyfinitem.h"
|
||||||
#include "jellyfinmediasource.h"
|
#include "jellyfinmediasource.h"
|
||||||
#include "serverdiscoverymodel.h"
|
#include "serverdiscoverymodel.h"
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@ void registerQml() {
|
||||||
|
|
||||||
// API models
|
// API models
|
||||||
Jellyfin::registerModels(QML_NAMESPACE);
|
Jellyfin::registerModels(QML_NAMESPACE);
|
||||||
|
Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
|
201
src/jellyfinitem.cpp
Normal file
201
src/jellyfinitem.cpp
Normal file
|
@ -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<JsonSerializable *>(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 *>()) {
|
||||||
|
JsonSerializable * obj = var.value<JsonSerializable *>();
|
||||||
|
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<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&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<Item>(URI, 1, 0, "JellyfinItem");
|
||||||
|
}
|
||||||
|
}
|
249
src/jellyfinitem.h
Normal file
249
src/jellyfinitem.h
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
#ifndef JELLYFINITEM_H
|
||||||
|
#define JELLYFINITEM_H
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QMetaProperty>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <QtQml>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#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<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;
|
||||||
|
|
||||||
|
// Handpicked, important ones
|
||||||
|
QString m_overview;
|
||||||
|
std::optional<int> m_productionYear = std::nullopt;
|
||||||
|
std::optional<int> m_indexNumber = std::nullopt;
|
||||||
|
};
|
||||||
|
|
||||||
|
void registerSerializableJsonTypes(const char* URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // JELLYFINITEM_H
|
|
@ -141,6 +141,13 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>Jellyfin::Item</name>
|
||||||
|
<message>
|
||||||
|
<source>Invalid response from the server: root element is not an object.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>LegalPage</name>
|
<name>LegalPage</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -218,6 +225,10 @@
|
||||||
<source>Retry</source>
|
<source>Retry</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Refresh</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>SeasonPage</name>
|
<name>SeasonPage</name>
|
||||||
|
|
Loading…
Reference in a new issue