1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2026-05-19 10:35:16 +00:00

Moved playback logic to C++-side (and refractoring)

This commit is contained in:
Chris Josten 2021-02-20 23:20:39 +01:00
parent 895731ae38
commit f7bca333c8
35 changed files with 1063 additions and 449 deletions

View file

@ -19,81 +19,70 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include "JellyfinQt/apimodel.h"
#include "JellyfinQt/DTO/item.h"
#include "JellyfinQt/DTO/userdata.h"
#include "JellyfinQt/DTO/user.h"
namespace Jellyfin {
ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel (parent),
// BaseApiModel
BaseApiModel::BaseApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel(parent),
m_path(path),
m_hasRecordResponse(hasRecordResponse),
m_addUserId(addUserId){
m_addUserId(addUserId) {
}
void ApiModel::reload() {
void BaseApiModel::setApiClient(ApiClient *apiClient) {
m_apiClient = apiClient;
emit apiClientChanged(m_apiClient);
}
void BaseApiModel::setLimit(int newLimit) {
m_limit = newLimit;
emit limitChanged(newLimit);
if (m_apiClient != nullptr && !m_isBeingParsed) {
load(LOAD_MORE);
}
}
void BaseApiModel::reload() {
this->setStatus(Loading);
m_startIndex = 0;
load(RELOAD);
}
void ApiModel::load(LoadType type) {
void BaseApiModel::load(LoadType type) {
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
if (m_apiClient == nullptr) {
qWarning() << "Please set the apiClient property before (re)loading";
return;
}
if (m_path.contains("{{user}}")) {
m_path = m_path.replace("{{user}}", m_apiClient->userId());
}
if (m_path.contains("{{show}}") && !m_show.isEmpty()) {
m_path = m_path.replace("{{show}}", m_show);
}
QString path(m_path);
replacePathPlaceholders(path);
QUrlQuery query;
if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
}
if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId);
}
if (!m_sortBy.empty()) {
query.addQueryItem("SortBy", m_sortBy.join(","));
}
if (m_sortOrder != Unspecified) {
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
}
if (!m_imageTypes.empty()) {
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
}
if (!m_includeItemTypes.empty()) {
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
}
if (!m_fields.empty()) {
query.addQueryItem("Fields", m_fields.join(","));
}
if (!m_seasonId.isEmpty()) {
query.addQueryItem("seasonId", m_seasonId);
}
if (m_addUserId) {
query.addQueryItem("userId", m_apiClient->userId());
}
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
addQueryParameters(query);
QNetworkReply *rep = m_apiClient->get(m_path, query);
QNetworkReply *rep = m_apiClient->get(path, query);
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
qDebug() << rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ": " << rep->request().url();
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
if (doc.isNull()) {
qWarning() << "JSON parse error";
this->setStatus(Error);
}
if (!m_hasRecordResponse) {
if (!doc.isArray()) {
qWarning() << "Object is not an array!";
this->setStatus(Error);
return;
}
this->m_array = doc.array();
QJsonArray items = doc.array();
setModelData(items);
} else {
if (!doc.isObject()) {
qWarning() << "Object is not an object!";
@ -125,42 +114,109 @@ void ApiModel::load(LoadType type) {
QJsonArray items = obj["Items"].toArray();
switch(type) {
case RELOAD:
this->m_array = items;
setModelData(items);
break;
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++) {
JsonHelper::convertToCamelCase(*it);
}
foreach (const QJsonValue &val, items) {
m_array.append(val);
}
this->endInsertRows();
appendModelData(items);
break;
}
}
if (type == RELOAD) {
generateFields();
}
this->setStatus(Ready);
rep->deleteLater();
});
}
void ApiModel::generateFields() {
if (m_array.size() == 0) return;
this->beginResetModel();
void BaseApiModel::addQueryParameters(QUrlQuery &query) {
if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
}
if (!m_sortBy.empty()) {
query.addQueryItem("SortBy", m_sortBy.join(","));
}
if (m_sortOrder != Unspecified) {
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
}
if (!m_fields.empty()) {
query.addQueryItem("Fields", m_fields.join(","));
}
if (m_addUserId) {
query.addQueryItem("userId", m_apiClient->userId());
}
}
void BaseApiModel::replacePathPlaceholders(QString &path) {
if (path.contains("{{user}}")) {
path = path.replace("{{user}}", m_apiClient->userId());
}
}
void BaseApiModel::classBegin() {
m_isBeingParsed = true;
}
void BaseApiModel::componentComplete() {
m_isBeingParsed = false;
}
// ApiModel
template <class T>
ApiModel<T>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
// If based on QObject, we know our role names before the first request
generateFields();
}
template <>
ApiModel<QJsonValue>::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: BaseApiModel(path, hasRecordResponse, addUserId, parent) {
// But we only know our role names after our first request.
}
template <class T>
T *ApiModel<T>::deserializeResult(QJsonValueRef source) {
T *result = new T(static_cast<BaseApiModel *>(this));
result->deserialize(source.toObject());
return result;
}
template <>
QJsonValue *ApiModel<QJsonValue>::deserializeResult(QJsonValueRef source) {
QJsonValue *result = new QJsonValue(source);
JsonHelper::convertToCamelCase(*result);
return result;
}
template <class T>
void ApiModel<T>::generateFields() {
const QMetaObject *obj = &T::staticMetaObject;
m_roles[Qt::UserRole + 1] = "qtObject";
for (int i = 0; i < obj->propertyCount(); i++) {
QMetaProperty property = obj->property(i);
m_roles.insert(Qt::UserRole + 2 + i, property.name());
}
}
template <>
void ApiModel<QJsonValue>::generateFields() {
// We can only generate field names if there is a first item. Redefining role names later leads to
// unexpected results, so prevent it as well.
if (m_array.size() == 0 || m_roles.size() > 0) return;
int i = Qt::UserRole + 1;
if (!m_array[0].isObject()) {
if (!m_array[0]->isObject()) {
qWarning() << "Iterator is not an object?";
return;
}
// Walks over the keys in the first record and adds them to the rolenames.
// This assumes the back-end has the same keys for every record. I could technically
// go over all records to be really sure, but no-one got time for a O(n²) algorithm, so
// go over all records to be really sure, but no-one got time for a O(n) algorithm, so
// this heuristic hopefully suffices.
QJsonObject ob = m_array[0].toObject();
QJsonObject ob = m_array[0]->toObject();
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
QString keyName = jt.key();
keyName[0] = keyName[0].toLower();
@ -169,20 +225,78 @@ void ApiModel::generateFields() {
m_roles.insert(i++, keyArr);
}
}
for (auto it = m_array.begin(); it != m_array.end(); it++){
JsonHelper::convertToCamelCase(*it);
}
template <class T>
void ApiModel<T>::setModelData(QJsonArray &data) {
this->beginResetModel();
for (T* value : m_array) {
value->deleteLater();
}
m_array.clear();
for(QJsonValueRef value : data) {
m_array.append(deserializeResult(value));
}
this->endResetModel();
}
QVariant ApiModel::data(const QModelIndex &index, int role) const {
template <>
void ApiModel<QJsonValue>::setModelData(QJsonArray &data) {
generateFields();
this->beginResetModel();
for (QJsonValue* value : m_array) {
delete value;
}
m_array.clear();
for(QJsonValueRef value : data) {
m_array.append(deserializeResult(value));
}
this->endResetModel();
}
template <class T>
void ApiModel<T>::appendModelData(QJsonArray &data) {
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + data.size() - 1);
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
for (auto it = data.begin(); it != data.end(); it++) {
JsonHelper::convertToCamelCase(*it);
}
for(QJsonValueRef val : data) {
m_array.append(deserializeResult(val));
}
this->endInsertRows();
}
template <class T>
QVariant ApiModel<T>::data(const QModelIndex &index, int role) const {
// Ignore roles we don't know
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
// Ignore invalid indices.
if (!index.isValid()) return QVariant();
QJsonObject obj = m_array.at(index.row()).toObject();
T* obj = m_array.at(index.row());
// m_roleNames[role] == "qtObject"
if (role == Qt::UserRole + 1) {
return QVariant::fromValue(obj);
}
const QString &key = m_roles[role];
if (role - Qt::UserRole - 2 < obj->metaObject()->propertyCount() ) {
return obj->property(key.toLocal8Bit());
}
return QVariant();
}
template <>
QVariant ApiModel<QJsonValue>::data(const QModelIndex &index, int role) const {
// Ignore roles we don't know
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
// Ignore invalid indices.
if (!index.isValid()) return QVariant();
QJsonObject obj = m_array.at(index.row())->toObject();
const QString &key = m_roles[role];
@ -192,7 +306,9 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
return QVariant();
}
bool ApiModel::canFetchMore(const QModelIndex &parent) const {
template <class T>
bool ApiModel<T>::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid()) return false;
switch(m_status) {
case Uninitialised:
@ -208,50 +324,99 @@ bool ApiModel::canFetchMore(const QModelIndex &parent) const {
} else {
return false;
}
}
void ApiModel::fetchMore(const QModelIndex &parent) {
template <class T>
void ApiModel<T>::fetchMore(const QModelIndex &parent) {
if (parent.isValid()) return;
this->setStatus(LoadingMore);
load(LOAD_MORE);
}
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
template <class T>
void ApiModel<T>::addQueryParameters(QUrlQuery &query) {
BaseApiModel::addQueryParameters(query);
}
template <class T>
void ApiModel<T>::replacePathPlaceholders(QString &path) {
BaseApiModel::replacePathPlaceholders(path);
}
template <class T>
void ApiModel<T>::insert(int index, T* object) {
Q_ASSERT(index >=0 && index <= size());
this->beginInsertRows(QModelIndex(), index, index);
m_array.insert(index, object);
this->endInsertRows();
}
template <class T>
void ApiModel<T>::removeAt(int index) {
this->beginRemoveRows(QModelIndex(), index, index);
m_array.removeAt(index);
this->endRemoveRows();
}
template <class T>
void ApiModel<T>::removeOne(T* object) {
int idx = m_array.indexOf(object);
if (idx >= 0) {
removeAt(idx);
}
}
// 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);
QObject::connect(this, &BaseApiModel::apiClientChanged, static_cast<BaseApiModel *>(this), [this](ApiClient *newApiClient) {
QObject::connect(newApiClient, &ApiClient::userDataChanged, this, &ItemModel::onUserDataChanged);
});
}
void ItemModel::onUserDataChanged(const QString &itemId, QSharedPointer<DTO::UserData> userData) {
void ItemModel::onUserDataChanged(const QString &itemId, DTO::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);
}
for (Item *val: m_array) {
if (val->userData() != nullptr && val->jellyfinId() == itemId) {
QModelIndex cell = this->index(i);
val->userData()->onUpdated(userData);
this->dataChanged(cell, cell);
}
i++;
}
}
void ItemModel::addQueryParameters(QUrlQuery &query) {
ApiModel<Item>::addQueryParameters(query);
if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId);
}
if (!m_imageTypes.empty()) {
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
}
if (!m_includeItemTypes.empty()) {
query.addQueryItem("IncludeItemTypes", m_includeItemTypes.join(","));
}
if (!m_seasonId.isEmpty()) {
query.addQueryItem("seasonId", m_seasonId);
}
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
}
void ItemModel::replacePathPlaceholders(QString &path) {
ApiModel::replacePathPlaceholders(path);
if (path.contains("{{show}}") && !m_show.isEmpty()) {
path = m_path.replace("{{show}}", m_show);
}
}
PublicUserModel::PublicUserModel(QObject *parent)
: ApiModel ("/users/public", false, false, parent) { }
UserViewModel::UserViewModel(QObject *parent)
: ApiModel ("/Users/{{user}}/Views", true, false, parent) {}
: ItemModel ("/Users/{{user}}/Views", true, false, parent) {}
UserItemModel::UserItemModel(QObject *parent)
: ItemModel ("/Users/{{user}}/Items", true, false, parent) {}
@ -272,7 +437,7 @@ ShowEpisodesModel::ShowEpisodesModel(QObject *parent)
: ItemModel ("/Shows/{{show}}/Episodes", true, true, parent) {}
void registerModels(const char *URI) {
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<BaseApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");