1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-18 12:02:41 +00:00
harbour-sailfin/core/src/jellyfinitem.cpp
Chris Josten d81fa50715 Models get updated when userData changes at server
The websocket now notifies the ApiClient, on which several models and
items are listening, when the userData for an user has changed. The UI
on the qml side may automatically updates without any extra effort.

This also resolves a bug where videos didn't resume after +/- 3:40 due
to an integer overflow.
2020-10-09 02:33:08 +02:00

327 lines
12 KiB
C++

/*
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
*/
#include "jellyfinitem.h"
namespace Jellyfin {
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
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;
// 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 if (QString(prop.name()).startsWith("__list__")) {
// QML doesn't like it if we expose properties of type QList<SubclassOfQobject *>
// we need to explicitly cast them to QList<QObject *> or QQmlListProperty, which sucks.
// That's why we have a special case for properties starting with __list__. This contains
// the actual QList<SubclassOfQobject *>, so that qml can access the object with its real name.
QString realName = toPascalCase(prop.name() + 8);
if (!jObj.contains(realName)) {
continue;
}
QJsonValue val = jObj[realName];
QMetaProperty realProp = obj->property(obj->indexOfProperty(prop.name() + 8));
if (!realProp.write(this, jsonToVariant(prop, val, jObj))) {
qDebug() << "Write to " << prop.name() << "failed";
};
}
}
}
QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) {
switch(val.type()) {
case QJsonValue::Null:
case QJsonValue::Undefined:
return QVariant();
case QJsonValue::Bool:
case QJsonValue::String:
return val.toVariant();
case QJsonValue::Double:
if (prop.type() == QVariant::LongLong) {
return static_cast<qint64>(val.toDouble(-1));
} if (prop.type() == QVariant::Int) {
return val.toInt();
} else {
return val.toDouble();
}
case QJsonValue::Array:
{
QJsonArray arr = val.toArray();
QVariantList varArr;
for (auto it = arr.constBegin(); it < arr.constEnd(); it++) {
QVariant variant = jsonToVariant(prop, *it, root);
varArr.append(variant);
}
return QVariant(varArr);
}
case QJsonValue::Object:
QJsonObject innerObj = val.toObject();
int typeNo = prop.userType();
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
if (metaType == nullptr) {
// Try to determine if the type is a qlist
QRegularExpressionMatch match = m_listExpression.match(prop.typeName());
if (match.hasMatch()) {
// It is a qList! Now extract the inner type
// There should be an easier way, shouldn't there?
QString listType = match.captured(1).prepend("Jellyfin::").append("*");
// UGLY CODE HERE WE COME
typeNo = QMetaType::type(listType.toUtf8());
if (typeNo == QMetaType::UnknownType) {
qDebug() << "Unknown type: " << listType;
return QVariant();
}
metaType = QMetaType::metaObjectForType(typeNo);
} else {
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
return QVariant();
}
}
QObject *deserializedInnerObj = metaType->newInstance();
deserializedInnerObj->setParent(this);
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
ser->deserialize(innerObj);
return QVariant(typeNo, &ser);
} else {
deserializedInnerObj->deleteLater();
qDebug() << "Object is not a serializable one!";
return QVariant();
}
}
return QVariant();
}
QJsonObject JsonSerializable::serialize(bool capitalize) 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[capitalize ? toPascalCase(prop.name()) : 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();
}
// MediaStream
MediaStream::MediaStream(QObject *parent) : JsonSerializable (parent) {}
MediaStream::MediaStream(const MediaStream &other)
: JsonSerializable (other.parent()),
m_codec(other.m_codec),
m_codecTag(other.m_codecTag),
m_language(other.m_language),
m_displayTitle(other.m_displayTitle),
m_type(other.m_type),
m_index(other.m_index){
}
bool MediaStream::operator==(const MediaStream &other) {
// displayTitle is explicitly left out, since it's generated based on other properties
// in the Jellyfin source code.
return m_codec == other.m_codec && m_codecTag == other.m_codecTag
&& m_language == other.m_language && m_type == other.m_type
&& m_index == other.m_index;
}
// UserData
UserData::UserData(QObject *parent) : JsonSerializable (parent) {}
void UserData::updateOnServer() {
//TODO: implement
}
void UserData::onUpdated(QSharedPointer<UserData> other) {
// The reason I'm not using setLikes and similar is that they don't work with std::nullopt,
// since QML does not like it.
// THe other reason is that the setLikes method will send a post request to the server, to update the contents
// we don't want that to happen, obviously, since the application could end in an infinite loop.
if (this->m_playedPercentage != other->m_playedPercentage) {
this->m_playedPercentage = other->m_playedPercentage;
emit playedPercentageChanged(playedPercentage());
}
if (m_playbackPositionTicks!= other->m_playbackPositionTicks) {
this->m_playbackPositionTicks = other->m_playbackPositionTicks;
emit playbackPositionTicksChanged(this->m_playbackPositionTicks);
}
if (m_isFavorite != other->m_isFavorite) {
this->m_isFavorite = other->m_isFavorite;
emit isFavoriteChanged(this->m_isFavorite);
}
if (this->m_likes != other->m_likes) {
this->m_likes = other->m_likes;
emit likesChanged(likes());
}
if (this->m_played != other->m_played) {
this->m_played = other->m_played;
emit playedChanged(this->m_played);
}
}
// Item
Item::Item(QObject *parent) : RemoteData(parent) {
connect(this, &RemoteData::apiClientChanged, this, [this](ApiClient *newApiClient) {
connect(newApiClient, &ApiClient::userDataChanged, this, &Item::onUserDataChanged);
});
}
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;
QString data(rep->readAll());
data = data.normalized(QString::NormalizationForm_D);
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &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 Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
if (itemId != m_id || m_userData == nullptr) return;
m_userData->onUpdated(userData);
}
void registerSerializableJsonTypes(const char* URI) {
qmlRegisterType<MediaStream>(URI, 1, 0, "MediaStream");
qmlRegisterType<UserData>(URI, 1, 0, "UserData");
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
}
}