Jellyfin Qt
QML Library for interacting with the Jellyfin multimedia server
Loading...
Searching...
No Matches
apimodel.h
Go to the documentation of this file.
1/*
2Sailfin: a Jellyfin client written using Qt
3Copyright (C) 2021 Chris Josten
4
5This library is free software; you can redistribute it and/or
6modify it under the terms of the GNU Lesser General Public
7License as published by the Free Software Foundation; either
8version 2.1 of the License, or (at your option) any later version.
9
10This library is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13Lesser General Public License for more details.
14
15You should have received a copy of the GNU Lesser General Public
16License along with this library; if not, write to the Free Software
17Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18*/
19
20#ifndef JELLYFIN_API_MODEL
21#define JELLYFIN_API_MODEL
22
23#include <optional>
24#include <typeinfo>
25
26#include <QAbstractListModel>
27#include <QFlags>
28#include <QFuture>
29#include <QFutureWatcher>
30#include <QMetaEnum>
31#include <QJsonArray>
32#include <QJsonDocument>
33#include <QJsonValue>
34#include <QtConcurrent/QtConcurrent>
35#include <QtQml>
36#include <QQmlParserStatus>
37#include <QVariant>
38
39#include "apiclient.h"
40#include "jsonhelper.h"
41
42#include "dto/baseitemdto.h"
43#include "dto/userdto.h"
44#include "dto/useritemdatadto.h"
46#include "loader/requesttypes.h"
47#include "support/loader.h"
49
50Q_DECLARE_LOGGING_CATEGORY(jellyfinApiModel)
51
52namespace Jellyfin {
53
54/*
55 * A brief description of this file:
56 *
57 * The reason why all of this is this complex is because Qt MOC's lack of support for template classes
58 * with Q_OBJECT. To work around this, "base classes", such as BaseModelLoader, are created which contain
59 * all functionallity required by Q_OBJECT. This class is extended by the templated class, which in turn
60 * is extended once more, so it can be registered for the QML engine.
61 *
62 * The loading of the data has beens separated from the QAbstractModels into ViewModel::Loaders
63 * to allow for loading over the network, loading from the cache and so on, without the QAbstractModel
64 * knowing anything about how it's done, except for the parameters it can pass to the loader and the result.
65 *
66 */
67
68class BaseModelLoader : public QObject, public QQmlParserStatus {
69 Q_INTERFACES(QQmlParserStatus)
70 Q_OBJECT
71public:
72 explicit BaseModelLoader(QObject *parent = nullptr);
74 Q_PROPERTY(Jellyfin::ViewModel::ModelStatusClass::Value status READ status NOTIFY statusChanged)
75 Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
76 Q_PROPERTY(bool autoReload READ autoReload WRITE setAutoReload NOTIFY autoReloadChanged)
77
78 ApiClient *apiClient() const { return m_apiClient; }
79 void setApiClient(ApiClient *newApiClient);
80
81 int limit() const { return m_limit; }
82 void setLimit(int newLimit);
83 bool autoReload() const { return m_autoReload; }
84 void setAutoReload(bool newAutoReload);
85
87
91 Q_INVOKABLE virtual void reload() {};
92
93 // QQmlParserStatus interface
94 virtual void classBegin() override;
95 virtual void componentComplete() override;
96
97 void autoReloadIfNeeded();
98signals:
99 void ready();
100 void apiClientChanged(ApiClient *newApiClient);
102 void limitChanged(int newLimit);
103 void autoReloadChanged(bool newAutoReload);
104
114protected:
115 // Is this object being parsed by the QML engine
116 bool m_isBeingParsed = false;
118 bool m_autoReload = true;
120 bool m_manualLimitSet = false;
121
122 // Query/record controlling properties
123 int m_limit = -1;
126 bool m_explicitLimitSet = false;
127 const int DEFAULT_LIMIT = 100;
129 void emitItemsLoaded() { emit itemsLoaded(); }
130
131 ViewModel::ModelStatus m_status = ViewModel::ModelStatus::Uninitialised;
133 if (this->m_status != newStatus) {
134 this->m_status = newStatus;
135 emit this->statusChanged();
136 if (this->m_status == ViewModel::ModelStatus::Ready) {
137 emit ready();
138 }
139 }
140 }
150 virtual bool canReload() const;
151};
152
157template <class T>
159public:
160 ModelLoader(QObject *parent = nullptr)
161 : BaseModelLoader(parent) {
162 }
163
164 void reload() override {
165 if (!canReload()) {
166 return;
167 }
168 m_startIndex = 0;
171 loadMore(ViewModel::ModelStatus::Loading);
172 }
173
174 void loadMore() {
175 if (!canReload()) {
176 return;
177 }
178 loadMore(ViewModel::ModelStatus::LoadingMore);
179 }
180
181 virtual bool canLoadMore() const {
183 }
184
191 std::pair<QList<T*>, int> &&result() { return std::move(m_result); }
192protected:
201 virtual void loadMore(ViewModel::ModelStatus suggestedStatus) = 0;
202 std::pair<QList<T*>, int> m_result;
203};
204
208template <class T, class R>
209QList<T> extractRecords(const R &result) {
210 Q_UNUSED(result)
211 qDebug() << "extractRecords not implemented for type " << typeid(R).name();
212 return QList<T>();
213}
214
215template <class R>
216int extractTotalRecordCount(const R &result) {
217 Q_UNUSED(result)
218 qDebug() << "extractTotalRecourdCount not implemented for type " << typeid(R).name();
219 return -1;
220}
221template <class P>
222void setRequestLimit(P &parameters, int limit) {
223 Q_UNUSED(parameters)
224 Q_UNUSED(limit)
225 qDebug() << "setRequestLimit not implemented for type " << typeid(P).name();
226}
227
231template <class P>
232bool setRequestStartIndex(P &parameters, int startIndex) {
233 Q_UNUSED(parameters)
234 Q_UNUSED(startIndex)
235 qDebug() << "setRequestStartIndex not implemented for type " << typeid(P).name();
236 return false;
237}
238
239#ifndef JELLYFIN_APIMODEL_CPP
240extern template bool setRequestStartIndex(Loader::GetUserViewsParams &params, int startIndex);
241extern template void setRequestLimit(Loader::GetUserViewsParams &params, int limit);
242
243extern template QList<DTO::BaseItemDto> extractRecords(const DTO::BaseItemDtoQueryResult &result);
244extern template int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result);
246extern template int extractTotalRecordCount(const QList<DTO::BaseItemDto> &result);
247
248extern template void setRequestLimit(Loader::GetLatestMediaParams &params, int limit);
249extern template bool setRequestStartIndex(Loader::GetLatestMediaParams &params, int offset);
250
251extern template void setRequestLimit(Loader::GetItemsByUserIdParams &params, int limit);
252extern template bool setRequestStartIndex(Loader::GetItemsByUserIdParams &params, int offset);
253
254extern template void setRequestLimit(Loader::GetResumeItemsParams &params, int limit);
255extern template bool setRequestStartIndex(Loader::GetResumeItemsParams &params, int offset);
256
257extern template void setRequestLimit(Loader::GetPublicUsersParams &params, int limit);
258extern template bool setRequestStartIndex(Loader::GetPublicUsersParams &params, int offset);
259
260extern template void setRequestLimit(Loader::GetNextUpParams &params, int limit);
261extern template bool setRequestStartIndex(Loader::GetNextUpParams &params, int offset);
262
263extern template void setRequestLimit(Loader::GetAlbumArtistsParams &params, int limit);
264extern template bool setRequestStartIndex(Loader::GetAlbumArtistsParams &params, int offset);
265
266extern template void setRequestLimit(Loader::GetLiveTvChannelsParams &params, int limit);
267extern template bool setRequestStartIndex(Loader::GetLiveTvChannelsParams &params, int offset);
268
269extern template QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result);
270extern template int extractTotalRecordCount(const QList<DTO::UserDto> &result);
271#endif
272
281template <class T, class D, class R, class P>
283public:
284 explicit LoaderModelLoader(Support::Loader<R, P> *loader, QObject *parent = nullptr)
285 : ModelLoader<T>(parent), m_loader(QScopedPointer<Support::Loader<R, P>>(loader)) {
286 this->connect(m_loader.data(), &Support::Loader<R, P>::ready, this, &LoaderModelLoader<T, D, R, P>::loaderReady);
287 this->connect(m_loader.data(), &Support::Loader<R, P>::error, this, &LoaderModelLoader<T, D, R, P>::loaderError);
288 }
289protected:
290 void loadMore(ViewModel::ModelStatus suggestedModelStatus) override {
291 // This method should only be callable on one thread.
292 // If futureWatcher's future is running, this method should not be called again.
293 if (m_loader->isRunning()) {
294 m_loader->cancel();
295 }
296 // Set an invalid result.
297 this->m_result = { QList<T*>(), -1 };
298
299 if (!setRequestStartIndex<P>(this->m_parameters, this->m_startIndex)
300 && suggestedModelStatus == ViewModel::ModelStatus::LoadingMore) {
301 // This loader's parameters does not setting a starting index,
302 // meaning loadMore is not supported.
303 return;
304 }
305 setRequestStartIndex<P>(this->m_parameters, this->m_startIndex);
306
307 if (suggestedModelStatus == ViewModel::ModelStatus::LoadingMore && this->m_explicitLimitSet) {
308 // If an explicit limit is set, we should load no more
309 return;
310 }
311
312 qCDebug(jellyfinApiModel) << "Explicit limit set: " << this->m_explicitLimitSet << ", " << this->m_limit;
313 if (this->m_explicitLimitSet) {
314 setRequestLimit<P>(this->m_parameters, this->m_limit);
315 } else {
316 setRequestLimit<P>(this->m_parameters, this->DEFAULT_LIMIT);
317 }
318 this->setStatus(suggestedModelStatus);
319
320 // We never want to set this while the loader is running, hence the Mutex and setting it here
321 // instead when Loader::setApiClient is called.
322 this->m_loader->setApiClient(this->m_apiClient);
323 this->m_loader->setParameters(this->m_parameters);
324 this->m_loader->load();
325 }
326
327 QScopedPointer<Support::Loader<R, P>> m_loader;
329
330 void loaderReady() {
331 try {
332 R result = m_loader->result();
333 QList<D> records = extractRecords<D, R>(result);
334 int totalRecordCount = extractTotalRecordCount<R>(result);
335 qDebug() << "Total record count: " << totalRecordCount << ", records in request: " << records.size();
336 // If totalRecordCount < 0, it is not supported for this endpoint
337 if (totalRecordCount < 0) {
338 totalRecordCount = records.size();
339 }
340 QList<T*> models;
341 models.reserve(records.size());
342
343 // Convert the DTOs into models
344 for (int i = 0; i < records.size(); i++) {
345 models.append(new T(records[i], m_loader->apiClient()));
346 }
347 this->setStatus(ViewModel::ModelStatus::Ready);
348 this->m_result = { models, this->m_startIndex};
349 this->m_startIndex += records.size();
350 this->m_totalRecordCount = totalRecordCount;
351 this->emitItemsLoaded();
352
353 } catch(QException &e) {
354 qWarning() << "Error while loading data: " << e.what();
355 this->setStatus(ViewModel::ModelStatus::Error);
356 }
357 }
358
359 void loaderError(QString error) {
360 Q_UNUSED(error)
361 this->setStatus(ViewModel::ModelStatus::Error);
362 }
363
364};
365
366class BaseApiModel : public QAbstractListModel {
367 Q_OBJECT
368public:
369 BaseApiModel(QObject *parent = nullptr)
370 : QAbstractListModel (parent) {}
371
372 Q_PROPERTY(BaseModelLoader *loader READ loader WRITE setLoader NOTIFY loaderChanged)
373
374 virtual BaseModelLoader *loader() const = 0;
375 virtual void setLoader(BaseModelLoader *newLoader) {
376 connect(newLoader, &BaseModelLoader::reloadWanted, this, &BaseApiModel::reload);
377 connect(newLoader, &BaseModelLoader::modelShouldClear, this, &BaseApiModel::clear);
379 emit loaderChanged();
380 };
382 if (oldLoader != nullptr) {
383 // Disconnect all signals
384 disconnect(oldLoader, nullptr, this, nullptr);
385 }
386 }
387public slots:
388 virtual void reload() = 0;
389 virtual void clear() = 0;
390protected slots:
391 virtual void loadingFinished() = 0;
392signals:
394};
395
404template <class T>
405class ApiModel : public BaseApiModel {
406public:
435 explicit ApiModel(QObject *parent = nullptr)
436 : BaseApiModel(parent) {
437 }
438
439 // Standard QAbstractItemModel overrides
440 int rowCount(const QModelIndex &index) const override {
441 if (!index.isValid()) return m_array.size();
442 return 0;
443 }
444
445 // QList-like API
446 QSharedPointer<T> at(int index) const { return m_array.at(index); }
450 int size() const {
451 return m_array.size();
452 }
453
454 void insert(int index, QSharedPointer<T> object) {
455 Q_ASSERT(index >= 0 && index <= size());
456 this->beginInsertRows(QModelIndex(), index, index);
457 m_array.insert(index, object);
458 this->endInsertRows();
459 }
460
461 void append(QSharedPointer<T> object) { insert(size(), object); }
462 //void append(T &object) { insert(size(), object); }
463 void append(QList<QSharedPointer<T>> &objects) {
464 int index = size();
465 this->beginInsertRows(QModelIndex(), index, index + objects.size() - 1);
466 m_array.append(objects);
467 this->endInsertRows();
468 };
469
470 QList<T*> mid(int pos, int length = -1) {
471 return m_array.mid(pos, length);
472 }
473
474 void removeAt(int index) {
475 Q_ASSERT(index < size());
476 this->beginRemoveRows(QModelIndex(), index, index);
477 m_array.removeAt(index);
478 this->endRemoveRows();
479 }
480
481 void removeUntilEnd(int from) {
482 this->beginRemoveRows(QModelIndex(), from , m_array.size());
483 while (m_array.size() != from) {
484 m_array.removeLast();
485 }
486 this->endRemoveRows();
487 }
488
489 void removeOne(QSharedPointer<T> object) {
490 int idx = m_array.indexOf(object);
491 if (idx >= 0) {
492 removeAt(idx);
493 }
494 }
495
496 void clear() override {
497 this->beginResetModel();
498 m_array.clear();
499 this->endResetModel();
500 }
501
503 return m_array;
504 }
505
506 // From AbstractListModel, gets implemented in ApiModel<T>
507 //virtual QHash<int, QByteArray> roleNames() const override = 0;
508 /*virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override = 0;*/
509 virtual bool canFetchMore(const QModelIndex &parent) const override {
510 if (parent.isValid()) return false;
511 if (m_loader == nullptr) return false;
512 return m_loader->canLoadMore();
513 }
514 virtual void fetchMore(const QModelIndex &parent) override {
515 if (parent.isValid()) return;
516 if (m_loader != nullptr) {
517 m_loader->loadMore();
518 }
519 }
520
521 BaseModelLoader *loader() const override {
522 return m_loader;
523 }
524
525 void setLoader(BaseModelLoader *newLoader) {
526 ModelLoader<T> *castedLoader = dynamic_cast<ModelLoader<T> *>(newLoader);
527 if (castedLoader != nullptr) {
528 // Hacky way to emit a signal
529 BaseApiModel::setLoader(newLoader);
531 m_loader = castedLoader;
532 reload();
533 } else {
534 qWarning() << "Somehow set a BaseModelLoader on ApiModel instead of a ModelLoader<T>";
535 }
536 }
537
538 void reload() override {
539 if (m_loader != nullptr) {
540 m_loader->reload();
541 }
542 }
543
544protected:
545 // Model-specific properties.
547 ModelLoader<T> *m_loader = nullptr;
548
549 void loadingFinished() override {
550 Q_ASSERT(m_loader != nullptr);
551 std::pair<QList<T*>, int> result = m_loader->result();
552 qDebug() << "Results loaded: index: " << result.second << ", count: " << result.first.size();
553 if (result.second == -1) {
554 clear();
555 } else if (result.second == m_array.size()) {
556 m_array.reserve(m_array.size() + result.second);
557 for (auto it = result.first.begin(); it != result.first.end(); it++) {
558 append(QSharedPointer<T>(*it));
559 }
560 } else if (result.second < m_array.size()){
561 removeUntilEnd(result.second);
562 m_array.reserve(m_array.size() + result.second);
563 for (auto it = result.first.begin(); it != result.first.end(); it++) {
564 append(QSharedPointer<T>(*it));
565 }
566 } else {
567 // result.second > m_array.size()
568 qWarning() << "Retrieved data from loader at position bigger than size()";
569 }
570 }
571private:
572 QMetaObject::Connection m_futureWatcherConnection;
573};
574
575}
576#endif //JELLYFIN_API_MODEL
An Api client for Jellyfin. Handles requests and authentication.
Definition apiclient.h:90
Abstract model for displaying collections.
Definition apimodel.h:405
void removeAt(int index)
Definition apimodel.h:474
virtual bool canFetchMore(const QModelIndex &parent) const override
Definition apimodel.h:509
void clear() override
Definition apimodel.h:496
void removeUntilEnd(int from)
Definition apimodel.h:481
QList< QSharedPointer< T > > m_array
Definition apimodel.h:546
void loadingFinished() override
Definition apimodel.h:549
void setLoader(BaseModelLoader *newLoader)
Definition apimodel.h:525
ApiModel(QObject *parent=nullptr)
Creates a new basemodel.
Definition apimodel.h:435
int rowCount(const QModelIndex &index) const override
Definition apimodel.h:440
void append(QList< QSharedPointer< T > > &objects)
Definition apimodel.h:463
void removeOne(QSharedPointer< T > object)
Definition apimodel.h:489
QList< T * > mid(int pos, int length=-1)
Definition apimodel.h:470
void append(QSharedPointer< T > object)
Definition apimodel.h:461
int size() const
Definition apimodel.h:450
void insert(int index, QSharedPointer< T > object)
Definition apimodel.h:454
const QList< QSharedPointer< T > > & toList()
Definition apimodel.h:502
void reload() override
Definition apimodel.h:538
virtual void fetchMore(const QModelIndex &parent) override
Definition apimodel.h:514
BaseModelLoader * loader() const override
Definition apimodel.h:521
QSharedPointer< T > at(int index) const
Definition apimodel.h:446
Definition apimodel.h:366
virtual void clear()=0
void disconnectOldLoader(BaseModelLoader *oldLoader)
Definition apimodel.h:381
virtual void reload()=0
Definition apimodel.cpp:94
virtual void loadingFinished()=0
BaseApiModel(QObject *parent=nullptr)
Definition apimodel.h:369
virtual void setLoader(BaseModelLoader *newLoader)
Definition apimodel.h:375
Definition apimodel.h:68
void modelShouldClear()
Emitted when the model should clear itself.
int limit() const
Definition apimodel.h:81
int m_totalRecordCount
Definition apimodel.h:125
int m_limit
Definition apimodel.h:123
void setLimit(int newLimit)
Definition apimodel.cpp:65
bool m_isBeingParsed
Definition apimodel.h:116
BaseModelLoader(QObject *parent=nullptr)
Definition apimodel.cpp:37
int limit
Definition apimodel.h:75
int m_startIndex
Definition apimodel.h:124
void limitChanged(int newLimit)
ApiClient * apiClient
Definition apimodel.h:73
virtual void classBegin() override
Definition apimodel.cpp:40
bool m_needsAuthentication
Definition apimodel.h:119
void setAutoReload(bool newAutoReload)
Definition apimodel.cpp:72
bool autoReload
Definition apimodel.h:76
ViewModel::ModelStatus status() const
Definition apimodel.h:86
const int DEFAULT_LIMIT
Definition apimodel.h:127
Jellyfin::ViewModel::ModelStatusClass::Value status
Definition apimodel.h:74
bool m_explicitLimitSet
Definition apimodel.h:126
void emitItemsLoaded()
Definition apimodel.h:129
void autoReloadIfNeeded()
Definition apimodel.cpp:49
virtual Q_INVOKABLE void reload()
Clears and reloads the model.
Definition apimodel.h:91
void apiClientChanged(ApiClient *newApiClient)
void emitModelShouldClear()
Definition apimodel.h:128
void autoReloadChanged(bool newAutoReload)
bool m_autoReload
Definition apimodel.h:118
ViewModel::ModelStatus m_status
Definition apimodel.h:131
virtual void componentComplete() override
Definition apimodel.cpp:44
void setApiClient(ApiClient *newApiClient)
Definition apimodel.cpp:56
void setStatus(ViewModel::ModelStatus newStatus)
Definition apimodel.h:132
bool m_manualLimitSet
Definition apimodel.h:120
virtual bool canReload() const
Determines if this model is able to reload.
Definition apimodel.cpp:83
ApiClient * m_apiClient
Definition apimodel.h:117
bool autoReload() const
Definition apimodel.h:83
Definition apimodel.h:282
void loaderError(QString error)
Definition apimodel.h:359
QScopedPointer< Support::Loader< R, P > > m_loader
Definition apimodel.h:327
P m_parameters
Definition apimodel.h:328
void loaderReady()
Definition apimodel.h:330
void loadMore(ViewModel::ModelStatus suggestedModelStatus) override
Loads data from the given offset with a maximum count of limit. The itemsLoaded() signal is emitted w...
Definition apimodel.h:290
LoaderModelLoader(Support::Loader< R, P > *loader, QObject *parent=nullptr)
Definition apimodel.h:284
Definition apimodel.h:158
virtual bool canLoadMore() const
Definition apimodel.h:181
virtual void loadMore(ViewModel::ModelStatus suggestedStatus)=0
Loads data from the given offset with a maximum count of limit. The itemsLoaded() signal is emitted w...
void reload() override
Clears and reloads the model.
Definition apimodel.h:164
void loadMore()
Definition apimodel.h:174
std::pair< QList< T * >, int > m_result
Definition apimodel.h:202
std::pair< QList< T * >, int > && result()
Holds the result. Moves it result to the caller and therefore can be only called once when the itemsL...
Definition apimodel.h:191
ModelLoader(QObject *parent=nullptr)
Definition apimodel.h:160
Definition loader.h:134
Value
Definition modelstatus.h:32
Definition mediaplayer2.h:20
QList< T > extractRecords(const R &result)
Definition apimodel.h:209
void setRequestLimit(P &parameters, int limit)
Definition apimodel.h:222
int extractTotalRecordCount(const R &result)
Definition apimodel.h:216
bool setRequestStartIndex(P &parameters, int startIndex)
Definition apimodel.h:232
void writeRequestTypesFile R(File headerFile, File implementationFile, R endpoints) if(is(ElementType!R
Definition openapigenerator.d:278
Q_DECLARE_LOGGING_CATEGORY(jellyfinWebSocket)