mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
parent
f028e38b7a
commit
4bbc86d31c
|
@ -108,9 +108,6 @@ signals:
|
||||||
*/
|
*/
|
||||||
void itemsLoaded();
|
void itemsLoaded();
|
||||||
void reloadWanted();
|
void reloadWanted();
|
||||||
public slots:
|
|
||||||
virtual void futureReady() = 0;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Is this object being parsed by the QML engine
|
// Is this object being parsed by the QML engine
|
||||||
bool m_isBeingParsed = false;
|
bool m_isBeingParsed = false;
|
||||||
|
@ -269,13 +266,16 @@ class LoaderModelLoader : public ModelLoader<T> {
|
||||||
public:
|
public:
|
||||||
explicit LoaderModelLoader(Support::Loader<R, P> *loader, QObject *parent = nullptr)
|
explicit LoaderModelLoader(Support::Loader<R, P> *loader, QObject *parent = nullptr)
|
||||||
: ModelLoader<T>(parent), m_loader(QScopedPointer<Support::Loader<R, P>>(loader)) {
|
: ModelLoader<T>(parent), m_loader(QScopedPointer<Support::Loader<R, P>>(loader)) {
|
||||||
QObject::connect(&m_futureWatcher, &QFutureWatcher<QList<T>>::finished, this, &BaseModelLoader::futureReady);
|
this->connect(m_loader.data(), &Support::Loader<R, P>::ready, this, &LoaderModelLoader<T, D, R, P>::loaderReady);
|
||||||
|
this->connect(m_loader.data(), &Support::Loader<R, P>::error, this, &LoaderModelLoader<T, D, R, P>::loaderError);
|
||||||
}
|
}
|
||||||
protected:
|
protected:
|
||||||
void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedModelStatus) override {
|
void loadMore(int offset, int limit, ViewModel::ModelStatus suggestedModelStatus) override {
|
||||||
// This method should only be callable on one thread.
|
// This method should only be callable on one thread.
|
||||||
// If futureWatcher's future is running, this method should not be called again.
|
// If futureWatcher's future is running, this method should not be called again.
|
||||||
if (m_futureWatcher.isRunning()) return;
|
if (m_loader->isRunning()) {
|
||||||
|
m_loader->cancel();
|
||||||
|
}
|
||||||
// Set an invalid result.
|
// Set an invalid result.
|
||||||
this->m_result = { QList<T*>(), -1 };
|
this->m_result = { QList<T*>(), -1 };
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ protected:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
setRequestLimit<P>(this->m_parameters, limit);
|
setRequestLimit<P>(this->m_parameters, limit);
|
||||||
}
|
}
|
||||||
this->setStatus(suggestedModelStatus);
|
this->setStatus(suggestedModelStatus);
|
||||||
|
|
||||||
|
@ -294,33 +294,14 @@ protected:
|
||||||
// instead when Loader::setApiClient is called.
|
// instead when Loader::setApiClient is called.
|
||||||
this->m_loader->setApiClient(this->m_apiClient);
|
this->m_loader->setApiClient(this->m_apiClient);
|
||||||
this->m_loader->setParameters(this->m_parameters);
|
this->m_loader->setParameters(this->m_parameters);
|
||||||
this->m_loader->prepareLoad();
|
this->m_loader->load();
|
||||||
QFuture<std::optional<R>> future = QtConcurrent::run(this->m_loader.data(), &Support::Loader<R, P>::load);
|
|
||||||
this->m_futureWatcher.setFuture(future);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QScopedPointer<Support::Loader<R, P>> m_loader;
|
QScopedPointer<Support::Loader<R, P>> m_loader;
|
||||||
QMutex m_mutex;
|
|
||||||
P m_parameters;
|
P m_parameters;
|
||||||
QFutureWatcher<std::optional<R>> m_futureWatcher;
|
|
||||||
|
|
||||||
void futureReady() override {
|
|
||||||
R result;
|
|
||||||
try {
|
|
||||||
std::optional<R> optResult = m_futureWatcher.result();
|
|
||||||
if (!optResult.has_value()) {
|
|
||||||
this->setStatus(ViewModel::ModelStatus::Error);
|
|
||||||
qWarning() << "ModelLoader returned with empty optional";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result = optResult.value();
|
|
||||||
} catch (Support::LoadException &e) {
|
|
||||||
qWarning() << "Exception while loading: " << e.what();
|
|
||||||
this->setStatus(ViewModel::ModelStatus::Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
void loaderReady() {
|
||||||
|
R result = m_loader->result();
|
||||||
QList<D> records = extractRecords<D, R>(result);
|
QList<D> records = extractRecords<D, R>(result);
|
||||||
int totalRecordCount = extractTotalRecordCount<R>(result);
|
int totalRecordCount = extractTotalRecordCount<R>(result);
|
||||||
qDebug() << "Total record count: " << totalRecordCount << ", records in request: " << records.size();
|
qDebug() << "Total record count: " << totalRecordCount << ", records in request: " << records.size();
|
||||||
|
@ -341,6 +322,11 @@ protected:
|
||||||
this->emitItemsLoaded();
|
this->emitItemsLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void loaderError(QString error) {
|
||||||
|
Q_UNUSED(error)
|
||||||
|
this->setStatus(ViewModel::ModelStatus::Error);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseApiModel : public QAbstractListModel {
|
class BaseApiModel : public QAbstractListModel {
|
||||||
|
@ -374,27 +360,7 @@ signals:
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Abstract model for displaying a REST JSON collection. Role names will be based on the fields encountered in the
|
* @brief Abstract model for displaying collections.
|
||||||
* first record.
|
|
||||||
*
|
|
||||||
* To create a new model, extend this class and create an QObject-parent constructor.
|
|
||||||
* Call the right super constructor with the right values, depending which path should be queried and
|
|
||||||
* how the result should be interpreted.
|
|
||||||
*
|
|
||||||
* Register the model in QML and create an instance. Don't forget to set the apiClient attribute or else
|
|
||||||
* the model you've created will be useless!
|
|
||||||
*
|
|
||||||
* Rolenames are based on the fields in the first object within the array of results, with the first letter
|
|
||||||
* lowercased, to accomodate for QML style guidelines. (This ain't C# here).
|
|
||||||
*
|
|
||||||
* If a call to /cats/new results in
|
|
||||||
* @code{.json}
|
|
||||||
* [
|
|
||||||
* {"Name": "meow", "Id": 432},
|
|
||||||
* {"Name": "miew", "Id": 323}
|
|
||||||
* ]
|
|
||||||
* @endcode
|
|
||||||
* The model will have roleNames for "name" and "id".
|
|
||||||
*
|
*
|
||||||
* @tparam T The class of the result.
|
* @tparam T The class of the result.
|
||||||
* @tparam R The class returned by the loader.
|
* @tparam R The class returned by the loader.
|
||||||
|
@ -567,24 +533,5 @@ private:
|
||||||
QMetaObject::Connection m_futureWatcherConnection;
|
QMetaObject::Connection m_futureWatcherConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief List of the public users on the server.
|
|
||||||
*/
|
|
||||||
/*class PublicUserModel : public ApiModel<QJsonValue> {
|
|
||||||
public:
|
|
||||||
explicit PublicUserModel (QObject *parent = nullptr);
|
|
||||||
};*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//template<>
|
|
||||||
//void ApiModel<Item>::apiClientChanged();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void registerModels(const char *URI);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif //JELLYFIN_API_MODEL
|
#endif //JELLYFIN_API_MODEL
|
||||||
|
|
|
@ -26,9 +26,12 @@
|
||||||
|
|
||||||
#include <QException>
|
#include <QException>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QObject>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include <QtConcurrent/QtConcurrent>
|
||||||
|
|
||||||
#include "../apiclient.h"
|
#include "../apiclient.h"
|
||||||
#include "jsonconv.h"
|
#include "jsonconv.h"
|
||||||
|
|
||||||
|
@ -54,6 +57,23 @@ private:
|
||||||
|
|
||||||
static const int HTTP_TIMEOUT = 30000; // 30 seconds;
|
static const int HTTP_TIMEOUT = 30000; // 30 seconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Base class for loaders that defines available signals.
|
||||||
|
*/
|
||||||
|
class LoaderBase : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* @brief Emitted when an error has occurred during loading and no result
|
||||||
|
* is available.
|
||||||
|
*/
|
||||||
|
void error(QString message = QString());
|
||||||
|
/**
|
||||||
|
* @brief Emitted when data was successfully loaded.
|
||||||
|
*/
|
||||||
|
void ready();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describing a way to load items. Used to abstract away
|
* Interface describing a way to load items. Used to abstract away
|
||||||
* the difference between loading from a cache or loading over the network.
|
* the difference between loading from a cache or loading over the network.
|
||||||
|
@ -71,24 +91,35 @@ static const int HTTP_TIMEOUT = 30000; // 30 seconds;
|
||||||
* be loaded.
|
* be loaded.
|
||||||
*/
|
*/
|
||||||
template <typename R, typename P>
|
template <typename R, typename P>
|
||||||
class Loader {
|
class Loader : public LoaderBase {
|
||||||
public:
|
public:
|
||||||
explicit Loader(ApiClient *apiClient)
|
explicit Loader(ApiClient *apiClient)
|
||||||
: m_apiClient(apiClient) {}
|
: m_apiClient(apiClient) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Called just before load() is called. In constrast to load,
|
* @brief load Loads the given resource asynchronously.
|
||||||
* this runs on the same thread as the ApiClient object.
|
|
||||||
*/
|
*/
|
||||||
virtual void prepareLoad() {};
|
virtual void load() {
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief load Loads the given resource. This usually run on a different thread.
|
|
||||||
* @return The resource if successfull.
|
|
||||||
*/
|
|
||||||
virtual std::optional<R> load() {
|
|
||||||
throw LoadException(QStringLiteral("Loader not set"));
|
throw LoadException(QStringLiteral("Loader not set"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieves the loaded resource. Only valid after the ready signal has been emitted.
|
||||||
|
* @return The loaded resource
|
||||||
|
*/
|
||||||
|
R result() const {
|
||||||
|
return m_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether this loader is already fetching a resource
|
||||||
|
*/
|
||||||
|
virtual void cancel() {}
|
||||||
|
|
||||||
|
bool isRunning() const {
|
||||||
|
return m_isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Heuristic to determine if this resource can be loaded via this loaded.
|
* @brief Heuristic to determine if this resource can be loaded via this loaded.
|
||||||
*
|
*
|
||||||
|
@ -103,6 +134,13 @@ public:
|
||||||
protected:
|
protected:
|
||||||
Jellyfin::ApiClient *m_apiClient;
|
Jellyfin::ApiClient *m_apiClient;
|
||||||
P m_parameters;
|
P m_parameters;
|
||||||
|
R m_result;
|
||||||
|
bool m_isRunning = false;
|
||||||
|
|
||||||
|
void stopWithError(QString message = QString()) {
|
||||||
|
m_isRunning = false;
|
||||||
|
emit this->error(message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,41 +150,24 @@ template <typename R, typename P>
|
||||||
class HttpLoader : public Loader<R, P> {
|
class HttpLoader : public Loader<R, P> {
|
||||||
public:
|
public:
|
||||||
explicit HttpLoader(Jellyfin::ApiClient *apiClient)
|
explicit HttpLoader(Jellyfin::ApiClient *apiClient)
|
||||||
: Loader<R, P> (apiClient) {}
|
: Loader<R, P> (apiClient) {
|
||||||
|
this->connect(&m_parsedWatcher, &QFutureWatcher<std::optional<R>>::finished, this, &HttpLoader<R, P>::onResponseParsed);
|
||||||
virtual void prepareLoad() override {
|
|
||||||
m_reply = this->m_apiClient->get(path(this->m_parameters), query(this->m_parameters));
|
|
||||||
m_requestFinishedConnection = QObject::connect(m_reply, &QNetworkReply::finished, [&]() { this->requestFinished(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual std::optional<R> load() override {
|
virtual void load() override {
|
||||||
Q_ASSERT_X(m_reply != nullptr, "HttpLoader::load", "prepareLoad() must be called before load()");
|
if (m_reply != nullptr) {
|
||||||
QMutexLocker locker(&m_mutex);
|
this->m_reply->deleteLater();
|
||||||
while (!m_reply->isFinished()) {
|
|
||||||
m_waitCondition.wait(&m_mutex);
|
|
||||||
}
|
}
|
||||||
QByteArray array = m_reply->readAll();
|
this->m_isRunning = true;
|
||||||
if (m_reply->error() != QNetworkReply::NoError) {
|
m_reply = this->m_apiClient->get(path(this->m_parameters), query(this->m_parameters));
|
||||||
|
this->connect(m_reply, &QNetworkReply::finished, this, &HttpLoader<R, P>::onRequestFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void cancel() override {
|
||||||
|
if (m_reply == nullptr) return;
|
||||||
|
if (m_reply->isRunning()) {
|
||||||
|
m_reply->abort();
|
||||||
m_reply->deleteLater();
|
m_reply->deleteLater();
|
||||||
//: An HTTP has occurred. First argument is replaced by QNetworkReply->errorString()
|
|
||||||
throw LoadException(QObject::tr("HTTP error: %1").arg(m_reply->errorString()));
|
|
||||||
}
|
|
||||||
m_reply->deleteLater();
|
|
||||||
m_reply = nullptr;
|
|
||||||
QJsonParseError error;
|
|
||||||
QJsonDocument document = QJsonDocument::fromJson(array, &error);
|
|
||||||
if (error.error != QJsonParseError::NoError) {
|
|
||||||
qWarning() << array;
|
|
||||||
throw LoadException(error.errorString().toLocal8Bit().constData());
|
|
||||||
}
|
|
||||||
if (document.isNull() || document.isEmpty()) {
|
|
||||||
return std::nullopt;
|
|
||||||
} else if (document.isArray()) {
|
|
||||||
return std::optional<R>(fromJsonValue<R>(document.array()));
|
|
||||||
} else if (document.isObject()){
|
|
||||||
return std::optional<R>(fromJsonValue<R>(document.object()));
|
|
||||||
} else {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,13 +188,49 @@ protected:
|
||||||
virtual QUrlQuery query(const P ¶meters) const = 0;
|
virtual QUrlQuery query(const P ¶meters) const = 0;
|
||||||
private:
|
private:
|
||||||
QNetworkReply *m_reply = nullptr;
|
QNetworkReply *m_reply = nullptr;
|
||||||
QWaitCondition m_waitCondition;
|
QFutureWatcher<std::optional<R>> m_parsedWatcher;
|
||||||
QMutex m_mutex;
|
|
||||||
QMetaObject::Connection m_requestFinishedConnection;
|
|
||||||
|
|
||||||
void requestFinished() {
|
void onRequestFinished() {
|
||||||
QObject::disconnect(m_requestFinishedConnection);
|
if (m_reply->error() != QNetworkReply::NoError) {
|
||||||
m_waitCondition.wakeAll();
|
m_reply->deleteLater();
|
||||||
|
//: An HTTP has occurred. First argument is replaced by QNetworkReply->errorString()
|
||||||
|
this->stopWithError(QStringLiteral("HTTP error: %1").arg(m_reply->errorString()));
|
||||||
|
}
|
||||||
|
QByteArray array = m_reply->readAll();
|
||||||
|
m_reply->deleteLater();
|
||||||
|
m_reply = nullptr;
|
||||||
|
m_parsedWatcher.setFuture(QtConcurrent::run(this, &HttpLoader<R, P>::parseResponse, array));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<R> parseResponse(QByteArray response) {
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(response, &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qWarning() << response;
|
||||||
|
this->stopWithError(error.errorString().toLocal8Bit().constData());
|
||||||
|
}
|
||||||
|
if (document.isNull() || document.isEmpty()) {
|
||||||
|
this->stopWithError(QStringLiteral("Unexpected empty JSON response"));
|
||||||
|
return std::nullopt;
|
||||||
|
} else if (document.isArray()) {
|
||||||
|
return std::make_optional<R>(fromJsonValue<R>(document.array()));
|
||||||
|
} else if (document.isObject()){
|
||||||
|
return std::make_optional<R>(fromJsonValue<R>(document.object()));
|
||||||
|
} else {
|
||||||
|
this->stopWithError(QStringLiteral("Unexpected JSON response"));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onResponseParsed() {
|
||||||
|
if (m_parsedWatcher.result().has_value()) {
|
||||||
|
R result = m_parsedWatcher.result().value();
|
||||||
|
this->m_result = result;
|
||||||
|
this->m_isRunning = false;
|
||||||
|
emit this->ready();
|
||||||
|
} else {
|
||||||
|
this->m_isRunning = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -153,24 +153,24 @@ public:
|
||||||
|
|
||||||
Loader(ApiClient *apiClient, Support::Loader<R, P> *loaderImpl, QObject *parent = nullptr)
|
Loader(ApiClient *apiClient, Support::Loader<R, P> *loaderImpl, QObject *parent = nullptr)
|
||||||
: LoaderBase(apiClient, parent),
|
: LoaderBase(apiClient, parent),
|
||||||
m_loader(loaderImpl),
|
m_loader(loaderImpl) {
|
||||||
m_futureWatcher(new QFutureWatcher<std::optional<R>>(this)) {
|
|
||||||
|
|
||||||
m_dataViewModel = new T(this);
|
m_dataViewModel = new T(this);
|
||||||
connect(m_futureWatcher, &RFutureWatcher::finished, this, &Loader<T, R, P>::updateData);
|
connect(m_loader.data(), &Support::LoaderBase::ready, this, &Loader<T, R, P>::onLoaderReady);
|
||||||
|
connect(m_loader.data(), &Support::LoaderBase::error, this, &Loader<T, R, P>::onLoaderError);
|
||||||
}
|
}
|
||||||
|
|
||||||
T *dataViewModel() const { return m_dataViewModel; }
|
T *dataViewModel() const { return m_dataViewModel; }
|
||||||
QObject *data() const override { return m_dataViewModel; }
|
QObject *data() const override { return m_dataViewModel; }
|
||||||
|
|
||||||
void reload() override {
|
void reload() override {
|
||||||
if (m_futureWatcher->isRunning()) return;
|
if (m_loader->isRunning()) {
|
||||||
|
m_loader->cancel();
|
||||||
|
};
|
||||||
setStatus(Loading);
|
setStatus(Loading);
|
||||||
this->m_loader->setApiClient(m_apiClient);
|
m_loader->setApiClient(m_apiClient);
|
||||||
m_loader->setParameters(m_parameters);
|
m_loader->setParameters(m_parameters);
|
||||||
m_loader->prepareLoad();
|
m_loader->load();
|
||||||
QFuture<std::optional<R>> future = QtConcurrent::run(this, &Loader<T, R, P>::invokeLoader);
|
|
||||||
m_futureWatcher->setFuture(future);
|
|
||||||
}
|
}
|
||||||
protected:
|
protected:
|
||||||
T* m_dataViewModel;
|
T* m_dataViewModel;
|
||||||
|
@ -180,46 +180,28 @@ protected:
|
||||||
*/
|
*/
|
||||||
QScopedPointer<Support::Loader<R, P>> m_loader = nullptr;
|
QScopedPointer<Support::Loader<R, P>> m_loader = nullptr;
|
||||||
private:
|
private:
|
||||||
QFutureWatcher<std::optional<R>> *m_futureWatcher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Callback for QtConcurrent::run()
|
|
||||||
* @param self Pointer to this class
|
|
||||||
* @param parameters Parameters to forward to the loader
|
|
||||||
* @return empty optional if an error occured, otherwise the result.
|
|
||||||
*/
|
|
||||||
std::optional<R> invokeLoader() {
|
|
||||||
QMutexLocker(&this->m_mutex);
|
|
||||||
try {
|
|
||||||
return this->m_loader->load();
|
|
||||||
} catch (Support::LoadException &e) {
|
|
||||||
qWarning() << "Exception while loading an item: " << e.what();
|
|
||||||
this->setErrorString(QString(e.what()));
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @brief Updates the data when finished.
|
* @brief Updates the data when finished.
|
||||||
*/
|
*/
|
||||||
void updateData() {
|
void onLoaderReady() {
|
||||||
std::optional<R> newDataOpt = m_futureWatcher->result();
|
R newData = m_loader->result();
|
||||||
if (newDataOpt.has_value()) {
|
if (m_dataViewModel->data()->sameAs(newData)) {
|
||||||
R newData = newDataOpt.value();
|
// Replace the data the model holds
|
||||||
if (m_dataViewModel->data()->sameAs(newData)) {
|
m_dataViewModel->data()->replaceData(newData);
|
||||||
// Replace the data the model holds
|
|
||||||
m_dataViewModel->data()->replaceData(newData);
|
|
||||||
} else {
|
|
||||||
// Replace the model
|
|
||||||
using PointerType = typename decltype(m_dataViewModel->data())::Type;
|
|
||||||
m_dataViewModel = new T(this, QSharedPointer<PointerType>::create(newData, m_apiClient));
|
|
||||||
}
|
|
||||||
setStatus(Ready);
|
|
||||||
emitDataChanged();
|
|
||||||
} else {
|
} else {
|
||||||
setStatus(Error);
|
// Replace the model
|
||||||
|
using PointerType = typename decltype(m_dataViewModel->data())::Type;
|
||||||
|
m_dataViewModel = new T(this, QSharedPointer<PointerType>::create(newData, m_apiClient));
|
||||||
}
|
}
|
||||||
|
setStatus(Ready);
|
||||||
|
emitDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLoaderError(QString message) {
|
||||||
|
setStatus(Error);
|
||||||
|
setErrorString(message);
|
||||||
}
|
}
|
||||||
QMutex m_mutex;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void registerRemoteTypes(const char *uri);
|
void registerRemoteTypes(const char *uri);
|
||||||
|
|
|
@ -70,7 +70,7 @@ class PlaybackManager : public QObject, public QQmlParserStatus {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_INTERFACES(QQmlParserStatus)
|
Q_INTERFACES(QQmlParserStatus)
|
||||||
public:
|
public:
|
||||||
using FetchCallback = std::function<void(QUrl &&, PlayMethod)>;
|
using ItemUrlLoader = Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams>;
|
||||||
|
|
||||||
explicit PlaybackManager(QObject *parent = nullptr);
|
explicit PlaybackManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
@ -179,13 +179,12 @@ private slots:
|
||||||
*/
|
*/
|
||||||
void updatePlaybackInfo();
|
void updatePlaybackInfo();
|
||||||
|
|
||||||
/// Called when the fetcherThread has fetched the playback URL and playSession
|
/// Called when we have fetched the playback URL and playSession
|
||||||
void onItemExtraDataReceived(const QString &itemId, const QUrl &url, const QString &playSession,
|
void onItemUrlReceived(const QString &itemId, const QUrl &url, const QString &playSession,
|
||||||
// Fully specify class to please MOC
|
// Fully specify class to please MOC
|
||||||
Jellyfin::DTO::PlayMethodClass::Value playMethod);
|
Jellyfin::DTO::PlayMethodClass::Value playMethod);
|
||||||
/// Called when the fetcherThread encountered an error
|
/// Called when we have encountered an error
|
||||||
void onItemErrorReceived(const QString &itemId, const QString &errorString);
|
void onItemErrorReceived(const QString &itemId, const QString &errorString);
|
||||||
void onDestroyed();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Factor to multiply with when converting from milliseconds to ticks.
|
/// Factor to multiply with when converting from milliseconds to ticks.
|
||||||
|
@ -228,9 +227,6 @@ private:
|
||||||
*/
|
*/
|
||||||
bool m_autoOpen = false;
|
bool m_autoOpen = false;
|
||||||
|
|
||||||
// Playback-related members
|
|
||||||
ItemUrlFetcherThread *m_urlFetcherThread;
|
|
||||||
|
|
||||||
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
QMediaPlayer::State m_oldState = QMediaPlayer::StoppedState;
|
||||||
PlayMethod m_playMethod = PlayMethod::Transcode;
|
PlayMethod m_playMethod = PlayMethod::Transcode;
|
||||||
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
QMediaPlayer::State m_playbackState = QMediaPlayer::StoppedState;
|
||||||
|
@ -252,6 +248,9 @@ private:
|
||||||
*/
|
*/
|
||||||
void postPlaybackInfo(PlaybackInfoType type);
|
void postPlaybackInfo(PlaybackInfoType type);
|
||||||
|
|
||||||
|
void requestItemUrl(QSharedPointer<Model::Item> item);
|
||||||
|
void handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response);
|
||||||
|
|
||||||
|
|
||||||
// QQmlParserListener interface
|
// QQmlParserListener interface
|
||||||
void classBegin() override { m_qmlIsParsingComponent = true; }
|
void classBegin() override { m_qmlIsParsingComponent = true; }
|
||||||
|
@ -262,57 +261,6 @@ private:
|
||||||
const qint64 PRELOAD_DURATION = 15 * 1000;
|
const qint64 PRELOAD_DURATION = 15 * 1000;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Thread that fetches the Item's stream URL always in the given order they were requested
|
|
||||||
class ItemUrlFetcherThread : public QThread {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
ItemUrlFetcherThread(PlaybackManager *manager);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Adds an item to the queue of items that should be requested
|
|
||||||
* @param item The item to fetch the URL of
|
|
||||||
*/
|
|
||||||
void addItemToQueue(QSharedPointer<Model::Item> item);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
/**
|
|
||||||
* @brief Emitted when the url of the item with the itemId has been retrieved.
|
|
||||||
* @param itemId The id of the item of which the URL has been retrieved
|
|
||||||
* @param itemUrl The retrieved url
|
|
||||||
* @param playSession The playsession set by the Jellyfin Server
|
|
||||||
*/
|
|
||||||
void itemUrlFetched(QString itemId, QUrl itemUrl, QString playSession, Jellyfin::DTO::PlayMethodClass::Value playMethod);
|
|
||||||
void itemUrlFetchError(QString itemId, QString errorString);
|
|
||||||
|
|
||||||
void prepareLoaderRequested(QPrivateSignal);
|
|
||||||
public slots:
|
|
||||||
/**
|
|
||||||
* @brief Ask the thread nicely to stop running.
|
|
||||||
*/
|
|
||||||
void cleanlyStop();
|
|
||||||
private slots:
|
|
||||||
void onPrepareLoader();
|
|
||||||
protected:
|
|
||||||
void run() override;
|
|
||||||
private:
|
|
||||||
PlaybackManager *m_parent;
|
|
||||||
Support::Loader<DTO::PlaybackInfoResponse, Jellyfin::Loader::GetPostedPlaybackInfoParams> *m_loader;
|
|
||||||
|
|
||||||
QMutex m_queueModifyMutex;
|
|
||||||
QQueue<QSharedPointer<Model::Item>> m_queue;
|
|
||||||
|
|
||||||
QMutex m_urlWaitConditionMutex;
|
|
||||||
/// WaitCondition on which this threads waits until an Item is put into the queue
|
|
||||||
QWaitCondition m_urlWaitCondition;
|
|
||||||
|
|
||||||
QMutex m_waitLoaderPreparedMutex;
|
|
||||||
/// WaitCondition on which this threads waits until the loader has been prepared.
|
|
||||||
QWaitCondition m_waitLoaderPrepared;
|
|
||||||
|
|
||||||
bool m_keepRunning = true;
|
|
||||||
bool m_loaderPrepared = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // NS ViewModel
|
} // NS ViewModel
|
||||||
} // NS Jellyfin
|
} // NS Jellyfin
|
||||||
|
|
||||||
|
|
|
@ -151,9 +151,4 @@ int extractTotalRecordCount(const QList<DTO::UserDto> &result) {
|
||||||
return result.size();
|
return result.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerModels(const char *URI) {
|
|
||||||
Q_UNUSED(URI)
|
|
||||||
//qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
|
|
||||||
//qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_item(nullptr),
|
m_item(nullptr),
|
||||||
m_mediaPlayer(new QMediaPlayer(this)),
|
m_mediaPlayer(new QMediaPlayer(this)),
|
||||||
m_urlFetcherThread(new ItemUrlFetcherThread(this)),
|
|
||||||
m_queue(new Model::Playlist(this)) {
|
m_queue(new Model::Playlist(this)) {
|
||||||
|
|
||||||
m_displayQueue = new ViewModel::Playlist(m_queue, this);
|
m_displayQueue = new ViewModel::Playlist(m_queue, this);
|
||||||
|
@ -48,10 +47,7 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
|
|
||||||
m_preloadTimer.setSingleShot(true);
|
m_preloadTimer.setSingleShot(true);
|
||||||
|
|
||||||
connect(this, &QObject::destroyed, this, &PlaybackManager::onDestroyed);
|
|
||||||
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
|
||||||
connect(m_urlFetcherThread, &ItemUrlFetcherThread::itemUrlFetched, this, &PlaybackManager::onItemExtraDataReceived);
|
|
||||||
m_urlFetcherThread->start();
|
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
connect(m_mediaPlayer, &QMediaPlayer::stateChanged, this, &PlaybackManager::mediaPlayerStateChanged);
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, &PlaybackManager::mediaPlayerPositionChanged);
|
||||||
|
@ -64,10 +60,6 @@ PlaybackManager::PlaybackManager(QObject *parent)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::onDestroyed() {
|
|
||||||
m_urlFetcherThread->cleanlyStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
void PlaybackManager::setApiClient(ApiClient *apiClient) {
|
||||||
if (!m_item.isNull()) {
|
if (!m_item.isNull()) {
|
||||||
m_item->setApiClient(apiClient);
|
m_item->setApiClient(apiClient);
|
||||||
|
@ -99,7 +91,7 @@ void PlaybackManager::setItem(QSharedPointer<Model::Item> newItem) {
|
||||||
// Deinitialize the streamUrl
|
// Deinitialize the streamUrl
|
||||||
if (shouldFetchStreamUrl) {
|
if (shouldFetchStreamUrl) {
|
||||||
setStreamUrl(QUrl());
|
setStreamUrl(QUrl());
|
||||||
m_urlFetcherThread->addItemToQueue(m_item);
|
requestItemUrl(m_item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,119 +278,68 @@ void PlaybackManager::componentComplete() {
|
||||||
m_qmlIsParsingComponent = false;
|
m_qmlIsParsingComponent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ItemUrlFetcherThread
|
void PlaybackManager::requestItemUrl(QSharedPointer<Model::Item> item) {
|
||||||
ItemUrlFetcherThread::ItemUrlFetcherThread(PlaybackManager *manager) :
|
ItemUrlLoader *loader = new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(m_apiClient);
|
||||||
QThread(manager),
|
Jellyfin::Loader::GetPostedPlaybackInfoParams params;
|
||||||
m_parent(manager),
|
params.setItemId(item->jellyfinId());
|
||||||
m_loader(new Jellyfin::Loader::HTTP::GetPostedPlaybackInfoLoader(manager->m_apiClient)) {
|
params.setUserId(m_apiClient->userId());
|
||||||
|
params.setEnableDirectPlay(true);
|
||||||
|
params.setEnableDirectStream(true);
|
||||||
|
params.setEnableTranscoding(true);
|
||||||
|
|
||||||
connect(this, &ItemUrlFetcherThread::prepareLoaderRequested, this, &ItemUrlFetcherThread::onPrepareLoader);
|
loader->setParameters(params);
|
||||||
|
connect(loader, &ItemUrlLoader::ready, [this, loader, item] {
|
||||||
|
DTO::PlaybackInfoResponse result = loader->result();
|
||||||
|
handlePlaybackInfoResponse(item->jellyfinId(), item->mediaType(), result);
|
||||||
|
loader->deleteLater();
|
||||||
|
});
|
||||||
|
connect(loader, &ItemUrlLoader::error, [this, loader, item](QString message) {
|
||||||
|
onItemErrorReceived(item->jellyfinId(), message);
|
||||||
|
loader->deleteLater();
|
||||||
|
});
|
||||||
|
loader->load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemUrlFetcherThread::addItemToQueue(QSharedPointer<Model::Item> item) {
|
void PlaybackManager::handlePlaybackInfoResponse(QString itemId, QString mediaType, DTO::PlaybackInfoResponse &response) {
|
||||||
QMutexLocker locker(&m_queueModifyMutex);
|
//TODO: move the item URL fetching logic out of this function, into MediaSourceInfo?
|
||||||
m_queue.enqueue(item);
|
QList<DTO::MediaSourceInfo> mediaSources = response.mediaSources();
|
||||||
m_urlWaitCondition.wakeOne();
|
QUrl resultingUrl;
|
||||||
}
|
QString playSession = response.playSessionId();
|
||||||
|
PlayMethod playMethod = PlayMethod::EnumNotSet;
|
||||||
void ItemUrlFetcherThread::cleanlyStop() {
|
for (int i = 0; i < mediaSources.size(); i++) {
|
||||||
m_keepRunning = false;
|
const DTO::MediaSourceInfo &source = mediaSources.at(i);
|
||||||
m_urlWaitCondition.wakeAll();
|
if (source.supportsDirectPlay() && QFile::exists(source.path())) {
|
||||||
}
|
resultingUrl = QUrl::fromLocalFile(source.path());
|
||||||
|
playMethod = PlayMethod::DirectPlay;
|
||||||
void ItemUrlFetcherThread::onPrepareLoader() {
|
} else if (source.supportsDirectStream()) {
|
||||||
m_loader->setApiClient(m_parent->m_apiClient);
|
if (mediaType == "Video") {
|
||||||
m_loader->prepareLoad();
|
mediaType.append('s');
|
||||||
m_loaderPrepared = true;
|
|
||||||
m_waitLoaderPrepared.wakeOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ItemUrlFetcherThread::run() {
|
|
||||||
while (m_keepRunning) {
|
|
||||||
m_urlWaitConditionMutex.lock();
|
|
||||||
while(m_queue.isEmpty() && m_keepRunning) {
|
|
||||||
m_urlWaitCondition.wait(&m_urlWaitConditionMutex);
|
|
||||||
}
|
|
||||||
m_urlWaitConditionMutex.unlock();
|
|
||||||
if (!m_keepRunning) break;
|
|
||||||
|
|
||||||
Jellyfin::Loader::GetPostedPlaybackInfoParams params;
|
|
||||||
QSharedPointer<Model::Item> item = m_queue.dequeue();
|
|
||||||
m_queueModifyMutex.lock();
|
|
||||||
params.setItemId(item->jellyfinId());
|
|
||||||
m_queueModifyMutex.unlock();
|
|
||||||
params.setUserId(m_parent->m_apiClient->userId());
|
|
||||||
params.setEnableDirectPlay(true);
|
|
||||||
params.setEnableDirectStream(true);
|
|
||||||
params.setEnableTranscoding(true);
|
|
||||||
|
|
||||||
m_loaderPrepared = false;
|
|
||||||
m_loader->setParameters(params);
|
|
||||||
|
|
||||||
// We cannot call m_loader->prepareLoad() from this thread, so we must
|
|
||||||
// emit a signal and hope for the best
|
|
||||||
emit prepareLoaderRequested(QPrivateSignal());
|
|
||||||
m_waitLoaderPreparedMutex.lock();
|
|
||||||
while (!m_loaderPrepared) {
|
|
||||||
m_waitLoaderPrepared.wait(&m_waitLoaderPreparedMutex);
|
|
||||||
}
|
|
||||||
m_waitLoaderPreparedMutex.unlock();
|
|
||||||
|
|
||||||
DTO::PlaybackInfoResponse response;
|
|
||||||
try {
|
|
||||||
std::optional<DTO::PlaybackInfoResponse> responseOpt = m_loader->load();
|
|
||||||
if (responseOpt.has_value()) {
|
|
||||||
response = responseOpt.value();
|
|
||||||
} else {
|
|
||||||
qWarning() << "Cannot retrieve URL of " << params.itemId();
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
} catch (QException &e) {
|
QUrlQuery query;
|
||||||
qWarning() << "Cannot retrieve URL of " << params.itemId() << ": " << e.what();
|
query.addQueryItem("mediaSourceId", source.jellyfinId());
|
||||||
continue;
|
query.addQueryItem("deviceId", m_apiClient->deviceId());
|
||||||
}
|
query.addQueryItem("api_key", m_apiClient->token());
|
||||||
|
query.addQueryItem("Static", "True");
|
||||||
//TODO: move the item URL fetching logic out of this function, into MediaSourceInfo?
|
resultingUrl = QUrl(m_apiClient->baseUrl() + "/" + mediaType + "/" + itemId
|
||||||
QList<DTO::MediaSourceInfo> mediaSources = response.mediaSources();
|
+ "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved));
|
||||||
QUrl resultingUrl;
|
playMethod = PlayMethod::DirectStream;
|
||||||
QString playSession = response.playSessionId();
|
} else if (source.supportsTranscoding()) {
|
||||||
PlayMethod playMethod = PlayMethod::EnumNotSet;
|
resultingUrl = QUrl(m_apiClient->baseUrl() + source.transcodingUrl());
|
||||||
for (int i = 0; i < mediaSources.size(); i++) {
|
playMethod = PlayMethod::Transcode;
|
||||||
const DTO::MediaSourceInfo &source = mediaSources.at(i);
|
|
||||||
if (source.supportsDirectPlay() && QFile::exists(source.path())) {
|
|
||||||
resultingUrl = QUrl::fromLocalFile(source.path());
|
|
||||||
playMethod = PlayMethod::DirectPlay;
|
|
||||||
} else if (source.supportsDirectStream()) {
|
|
||||||
QString mediaType = item->mediaType();
|
|
||||||
if (mediaType == "Video") {
|
|
||||||
mediaType.append('s');
|
|
||||||
}
|
|
||||||
QUrlQuery query;
|
|
||||||
query.addQueryItem("mediaSourceId", source.jellyfinId());
|
|
||||||
query.addQueryItem("deviceId", m_parent->m_apiClient->deviceId());
|
|
||||||
query.addQueryItem("api_key", m_parent->m_apiClient->token());
|
|
||||||
query.addQueryItem("Static", "True");
|
|
||||||
resultingUrl = QUrl(this->m_parent->m_apiClient->baseUrl() + "/" + mediaType + "/" + params.itemId()
|
|
||||||
+ "/stream." + source.container() + "?" + query.toString(QUrl::EncodeReserved));
|
|
||||||
playMethod = PlayMethod::DirectStream;
|
|
||||||
} else if (source.supportsTranscoding()) {
|
|
||||||
resultingUrl = QUrl(m_parent->m_apiClient->baseUrl() + source.transcodingUrl());
|
|
||||||
playMethod = PlayMethod::Transcode;
|
|
||||||
} else {
|
|
||||||
qDebug() << "No suitable sources for item " << item->jellyfinId();
|
|
||||||
}
|
|
||||||
if (!resultingUrl.isEmpty()) break;
|
|
||||||
}
|
|
||||||
if (resultingUrl.isEmpty()) {
|
|
||||||
qWarning() << "Could not find suitable media source for item " << params.itemId();
|
|
||||||
emit itemUrlFetchError(item->jellyfinId(), tr("Cannot fetch stream URL"));
|
|
||||||
} else {
|
} else {
|
||||||
emit itemUrlFetched(item->jellyfinId(), resultingUrl, playSession, playMethod);
|
qDebug() << "No suitable sources for item " << itemId;
|
||||||
}
|
}
|
||||||
|
if (!resultingUrl.isEmpty()) break;
|
||||||
|
}
|
||||||
|
if (resultingUrl.isEmpty()) {
|
||||||
|
qWarning() << "Could not find suitable media source for item " << itemId;
|
||||||
|
onItemErrorReceived(itemId, tr("Cannot fetch stream URL"));
|
||||||
|
} else {
|
||||||
|
onItemUrlReceived(itemId, resultingUrl, playSession, playMethod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaybackManager::onItemExtraDataReceived(const QString &itemId, const QUrl &url,
|
void PlaybackManager::onItemUrlReceived(const QString &itemId, const QUrl &url,
|
||||||
const QString &playSession, PlayMethod playMethod) {
|
const QString &playSession, PlayMethod playMethod) {
|
||||||
Q_UNUSED(url)
|
Q_UNUSED(url)
|
||||||
Q_UNUSED(playSession)
|
Q_UNUSED(playSession)
|
||||||
|
@ -413,8 +354,9 @@ void PlaybackManager::onItemExtraDataReceived(const QString &itemId, const QUrl
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "Late reply for " << itemId << " received, ignoring";
|
qDebug() << "Late reply for " << itemId << " received, ignoring";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Called when the fetcherThread encountered an error
|
/// Called when the fetcherThread encountered an error
|
||||||
void PlaybackManager::onItemErrorReceived(const QString &itemId, const QString &errorString) {
|
void PlaybackManager::onItemErrorReceived(const QString &itemId, const QString &errorString) {
|
||||||
Q_UNUSED(itemId)
|
Q_UNUSED(itemId)
|
||||||
|
|
Loading…
Reference in a new issue