mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-05 18:22:46 +00:00
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.
This commit is contained in:
parent
1e80ceb697
commit
d81fa50715
17 changed files with 304 additions and 44 deletions
|
@ -275,6 +275,10 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
|
|||
rep->deleteLater();
|
||||
}
|
||||
|
||||
void ApiClient::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
||||
userDataChanged(itemId, userData);
|
||||
}
|
||||
|
||||
void ApiClient::setAuthenticated(bool authenticated) {
|
||||
this->m_authenticated = authenticated;
|
||||
if (authenticated) m_webSocket->open();
|
||||
|
|
|
@ -125,6 +125,9 @@ void ApiModel::load(LoadType type) {
|
|||
case LOAD_MORE:
|
||||
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
|
||||
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
|
||||
for (auto it = items.begin(); it != items.end(); it++) {
|
||||
convertToCamelCase(*it);
|
||||
}
|
||||
foreach (const QJsonValue &val, items) {
|
||||
m_array.append(val);
|
||||
}
|
||||
|
@ -159,9 +162,11 @@ void ApiModel::generateFields() {
|
|||
QByteArray keyArr = keyName.toUtf8();
|
||||
if (!m_roles.values().contains(keyArr)) {
|
||||
m_roles.insert(i++, keyArr);
|
||||
//qDebug() << m_path << " adding " << keyName << " as " << ( i - 1);
|
||||
}
|
||||
}
|
||||
for (auto it = m_array.begin(); it != m_array.end(); it++){
|
||||
convertToCamelCase(*it);
|
||||
}
|
||||
this->endResetModel();
|
||||
}
|
||||
|
||||
|
@ -174,8 +179,7 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
|
|||
|
||||
QJsonObject obj = m_array.at(index.row()).toObject();
|
||||
|
||||
QString key = m_roles[role];
|
||||
key[0] = key[0].toUpper();
|
||||
const QString &key = m_roles[role];
|
||||
if (obj.contains(key)) {
|
||||
return obj[key].toVariant();
|
||||
}
|
||||
|
@ -208,6 +212,64 @@ void ApiModel::fetchMore(const QModelIndex &parent) {
|
|||
|
||||
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
|
||||
|
||||
void ApiModel::convertToCamelCase(QJsonValueRef val) {
|
||||
switch(val.type()) {
|
||||
case QJsonValue::Object: {
|
||||
QJsonObject obj = val.toObject();
|
||||
for(const QString &key: obj.keys()) {
|
||||
QJsonValueRef ref = obj[key];
|
||||
convertToCamelCase(ref);
|
||||
obj[convertToCamelCaseHelper(key)] = ref;
|
||||
obj.remove(key);
|
||||
}
|
||||
val = obj;
|
||||
break;
|
||||
}
|
||||
case QJsonValue::Array: {
|
||||
QJsonArray arr = val.toArray();
|
||||
for (auto it = arr.begin(); it != arr.end(); it++) {
|
||||
convertToCamelCase(*it);
|
||||
}
|
||||
val = arr;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString ApiModel::convertToCamelCaseHelper(const QString &str) {
|
||||
QString res(str);
|
||||
res[0] = res[0].toLower();
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// Itemmodel
|
||||
|
||||
ItemModel::ItemModel(QString path, bool hasRecordFields, bool replaceUser, QObject *parent)
|
||||
: ApiModel (path, hasRecordFields, replaceUser, parent){
|
||||
connect(this, &ApiModel::apiClientChanged, this, [this](ApiClient *newApiClient) {
|
||||
connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
|
||||
});
|
||||
}
|
||||
|
||||
void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
||||
int i = 0;
|
||||
for (QJsonValueRef val: m_array) {
|
||||
QJsonObject item = val.toObject();
|
||||
if (item.contains("id") && item["id"].toString() == itemId) {
|
||||
if (item.contains("userData")) {
|
||||
QModelIndex cell = this->index(i);
|
||||
item["userData"] = userData->serialize(false);
|
||||
val = item;
|
||||
this->dataChanged(cell, cell);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void registerModels(const char *URI) {
|
||||
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
||||
|
|
|
@ -1,3 +1,22 @@
|
|||
/*
|
||||
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 {
|
||||
|
@ -15,7 +34,6 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
|||
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"];
|
||||
|
@ -30,17 +48,13 @@ void JsonSerializable::deserialize(const QJsonObject &jObj) {
|
|||
// 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)) {
|
||||
qDebug() << "Ignoring " << realName << " - " << prop.name();
|
||||
continue;
|
||||
}
|
||||
QJsonValue val = jObj[realName];
|
||||
qDebug() << realName << " - " << prop.name() << ": " << val;
|
||||
QMetaProperty realProp = obj->property(obj->indexOfProperty(prop.name() + 8));
|
||||
if (!realProp.write(this, jsonToVariant(prop, val, jObj))) {
|
||||
qDebug() << "Write to " << prop.name() << "failed";
|
||||
};
|
||||
} else {
|
||||
qDebug() << "Ignored " << prop.name() << " while deserializing";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,19 +65,24 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
|
|||
case QJsonValue::Undefined:
|
||||
return QVariant();
|
||||
case QJsonValue::Bool:
|
||||
case QJsonValue::Double:
|
||||
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);
|
||||
qDebug() << variant;
|
||||
varArr.append(variant);
|
||||
}
|
||||
qDebug() << prop.name() << ": " << varArr.count();
|
||||
return QVariant(varArr);
|
||||
}
|
||||
case QJsonValue::Object:
|
||||
|
@ -104,7 +123,7 @@ QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &v
|
|||
return QVariant();
|
||||
}
|
||||
|
||||
QJsonObject JsonSerializable::serialize() const {
|
||||
QJsonObject JsonSerializable::serialize(bool capitalize) const {
|
||||
QJsonObject result;
|
||||
const QMetaObject *obj = this->metaObject();
|
||||
for (int i = 0; i < obj->propertyCount(); i++) {
|
||||
|
@ -112,7 +131,7 @@ QJsonObject JsonSerializable::serialize() const {
|
|||
if (QString(prop.name()) == "jellyfinId") {
|
||||
result["Id"] = variantToJson(prop.read(this));
|
||||
} else {
|
||||
result[toPascalCase(prop.name())] = variantToJson(prop.read(this));
|
||||
result[capitalize ? toPascalCase(prop.name()) : prop.name()] = variantToJson(prop.read(this));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -206,9 +225,47 @@ bool MediaStream::operator==(const MediaStream &other) {
|
|||
&& 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) {}
|
||||
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) {
|
||||
|
@ -256,8 +313,14 @@ void Item::reload() {
|
|||
});
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,9 @@ void WebSocket::textMessageReceived(const QString &message) {
|
|||
MessageType messageType = static_cast<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().keyToValue(messageTypeStr.toLatin1(), &ok));
|
||||
if (!ok) {
|
||||
qWarning() << "Unknown message arrived: " << messageTypeStr;
|
||||
if (messageRoot.contains("Data")) {
|
||||
qDebug() << "with data: " << QJsonDocument(messageRoot["Data"].toObject()).toJson();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -82,6 +85,21 @@ void WebSocket::textMessageReceived(const QString &message) {
|
|||
case KeepAlive:
|
||||
//TODO: do something?
|
||||
break;
|
||||
case UserDataChanged: {
|
||||
QJsonObject data2 = data.toObject();
|
||||
if (data2["UserId"] != m_apiClient->userId()) {
|
||||
qDebug() << "Received UserDataCHanged for other user";
|
||||
break;
|
||||
}
|
||||
QJsonArray userDataList = data2["UserDataList"].toArray();
|
||||
for (QJsonValue val: userDataList) {
|
||||
QSharedPointer<UserData> userData(new UserData, &QObject::deleteLater);
|
||||
userData->deserialize(val.toObject());
|
||||
m_apiClient->onUserDataChanged(userData->itemId(), userData);
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue