2020-09-27 18:38:33 +00:00
|
|
|
/*
|
|
|
|
Sailfin: a Jellyfin client written using Qt
|
2021-02-17 18:42:10 +00:00
|
|
|
Copyright (C) 2021 Chris Josten
|
2020-09-27 18:38:33 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
#ifndef JELLYFIN_API_CLIENT
|
|
|
|
#define JELLYFIN_API_CLIENT
|
|
|
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
#include <QJsonDocument>
|
|
|
|
#include <QJsonObject>
|
|
|
|
#include <QJsonParseError>
|
|
|
|
#include <QJsonValue>
|
|
|
|
|
2020-10-08 01:00:08 +00:00
|
|
|
#include <QHostInfo>
|
2020-09-15 14:53:13 +00:00
|
|
|
#include <QObject>
|
2021-03-24 19:04:03 +00:00
|
|
|
#include <QQmlListProperty>
|
2020-09-15 14:53:13 +00:00
|
|
|
#include <QString>
|
2020-09-25 12:46:39 +00:00
|
|
|
#include <QSysInfo>
|
2020-09-15 14:53:13 +00:00
|
|
|
#include <QtQml>
|
|
|
|
#include <QUuid>
|
|
|
|
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QUrlQuery>
|
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
#include "dto/generalcommandtype.h"
|
2020-09-15 14:53:13 +00:00
|
|
|
#include "credentialmanager.h"
|
2021-03-28 02:00:00 +00:00
|
|
|
#include "model/deviceprofile.h"
|
2021-03-24 19:04:03 +00:00
|
|
|
#include "eventbus.h"
|
2020-09-25 12:46:39 +00:00
|
|
|
|
|
|
|
namespace Jellyfin {
|
2021-02-13 23:21:49 +00:00
|
|
|
class PlaybackManager;
|
2021-03-28 02:00:00 +00:00
|
|
|
class WebSocket;
|
2021-02-17 18:42:10 +00:00
|
|
|
|
|
|
|
namespace DTO {
|
2021-02-21 04:02:05 +00:00
|
|
|
class UserItemDataDto; // Keep it as an opaque pointer
|
|
|
|
using UserData = UserItemDataDto;
|
2021-02-17 18:42:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
using namespace DTO;
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/**
|
|
|
|
* @brief An Api client for Jellyfin. Handles requests and authentication.
|
|
|
|
*
|
|
|
|
* This class should also be given to certain models and other sources, so they are able to make
|
|
|
|
* requests to the correct server.
|
|
|
|
*
|
|
|
|
* General usage is as follows:
|
|
|
|
* 1. (Optional) Call restoreSavedSession(). This will try to load previously saved credentials and connect to the server.
|
|
|
|
* If all succeeds, the property authenticated should be set to true and its signal should be emitted. All is done.
|
|
|
|
* If it fails, setupRequired will be emitted. Continue following these steps.
|
|
|
|
* 2. If opting in to manually manage the session or restoreSavedSession() failed, you'll need to set the property
|
|
|
|
* baseUrl to the root of the Jellyfin server, e.g. "https://jellyfin.example.com:8098", so not the url to the
|
|
|
|
* web interface! Nearby servers can be discovered using Jellyfin::ServerDiscoveryModel.
|
|
|
|
* 3. Call ::setupConnection(). First of all, the client will try to resolve any redirects and will update
|
|
|
|
* the baseUrl property if following redirects. Then it will emit connectionSuccess(QString). The QString from
|
|
|
|
* the signal contains a user-oriented login message configured by the user that should be displayed in the URL
|
|
|
|
* somewhere.
|
|
|
|
* 4. After ::connected is emitted, call ::authenticate(QString, QString, bool). with the username and password.
|
|
|
|
* The last boolean argument is used if you want to have the ApiClient store your credentials, so that they
|
|
|
|
* later can be used with restoreSavedSession().
|
|
|
|
* 5. If the authenticated property is set to true, you are now authenticated! If loginError() is emitted, you aren't and
|
|
|
|
* you should go back to step 4.
|
|
|
|
*
|
|
|
|
* These steps might change. I'm considering decoupling CredentialsManager from this class to clean some code up.
|
|
|
|
*/
|
|
|
|
class ApiClient : public QObject {
|
2020-10-02 10:20:54 +00:00
|
|
|
friend class WebSocket;
|
2021-02-13 23:21:49 +00:00
|
|
|
friend class PlaybackManager;
|
2020-09-15 14:53:13 +00:00
|
|
|
Q_OBJECT
|
|
|
|
public:
|
2020-09-25 12:46:39 +00:00
|
|
|
explicit ApiClient(QObject *parent = nullptr);
|
|
|
|
Q_PROPERTY(QString baseUrl MEMBER m_baseUrl READ baseUrl NOTIFY baseUrlChanged)
|
2020-09-15 14:53:13 +00:00
|
|
|
Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged)
|
|
|
|
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
2021-02-13 20:42:57 +00:00
|
|
|
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfile NOTIFY deviceProfileChanged)
|
2020-09-27 20:41:35 +00:00
|
|
|
Q_PROPERTY(QString version READ version)
|
2021-03-24 19:04:03 +00:00
|
|
|
Q_PROPERTY(EventBus *eventbus READ eventbus FINAL)
|
|
|
|
Q_PROPERTY(WebSocket *websocket READ websocket FINAL)
|
2021-03-26 20:27:35 +00:00
|
|
|
Q_PROPERTY(QVariantList supportedCommands READ supportedCommands WRITE setSupportedCommands NOTIFY supportedCommandsChanged)
|
2021-03-25 16:32:00 +00:00
|
|
|
/**
|
|
|
|
* Wether this ApiClient operates in "online mode".
|
|
|
|
*
|
|
|
|
* When operating in offline mode, this client will not make network requests and only use a local cache, making
|
|
|
|
* certain features unavailable.
|
|
|
|
*/
|
|
|
|
Q_PROPERTY(bool online READ online NOTIFY onlineChanged)
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
/*QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination,
|
|
|
|
QVariantMap filters, QStringList fields, QStringList expand, QString id);*/
|
|
|
|
|
|
|
|
bool authenticated() const { return m_authenticated; }
|
|
|
|
void setBaseUrl(QString url) {
|
|
|
|
this->m_baseUrl = url;
|
|
|
|
if (this->m_baseUrl.endsWith("/")) {
|
|
|
|
this->m_baseUrl.chop(1);
|
|
|
|
}
|
|
|
|
emit this->baseUrlChanged(m_baseUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
QNetworkReply *get(const QString &path, const QUrlQuery ¶ms = QUrlQuery());
|
2020-09-25 12:46:39 +00:00
|
|
|
QNetworkReply *post(const QString &path, const QJsonDocument &data = QJsonDocument(), const QUrlQuery ¶ms = QUrlQuery());
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
enum ApiError {
|
|
|
|
JSON_ERROR,
|
|
|
|
UNEXPECTED_REPLY,
|
|
|
|
UNEXPECTED_STATUS,
|
|
|
|
INVALID_PASSWORD
|
|
|
|
};
|
2021-03-28 02:00:00 +00:00
|
|
|
Q_ENUM(ApiError)
|
2020-09-15 14:53:13 +00:00
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
const QString &baseUrl() const { return this->m_baseUrl; }
|
|
|
|
const QString &userId() const { return m_userId; }
|
|
|
|
const QString &deviceId() const { return m_deviceId; }
|
|
|
|
/**
|
|
|
|
* @brief QML applications can set this type to indicate which commands they support.
|
|
|
|
*
|
|
|
|
* These commands can be sent by other Jellyfin clients to instruct this Jellyfin client to play a
|
|
|
|
* certain item, control playback and so on.
|
|
|
|
*
|
|
|
|
* This property must be set before restoreSavedSession() is called and not be changed afterwards.
|
|
|
|
* The list support commands will be sent to the Jellyfin server. QML applications should listen to
|
|
|
|
* the events emitted by the eventBus and act accordingly.
|
|
|
|
*/
|
2021-03-26 20:27:35 +00:00
|
|
|
QVariantList supportedCommands() const { return m_supportedCommands; }
|
|
|
|
void setSupportedCommands(QVariantList newSupportedCommands) { m_supportedCommands = newSupportedCommands; emit supportedCommandsChanged(); }
|
2020-09-25 12:46:39 +00:00
|
|
|
QJsonObject &deviceProfile() { return m_deviceProfile; }
|
|
|
|
QJsonObject &playbackDeviceProfile() { return m_playbackDeviceProfile; }
|
2021-03-24 19:04:03 +00:00
|
|
|
/**
|
|
|
|
* @brief Retrieves the authentication token. Null QString if not authenticated.
|
|
|
|
* @note This is not the full authentication header, just the token.
|
|
|
|
*/
|
|
|
|
QString &token() { return m_token; }
|
2020-10-08 01:00:08 +00:00
|
|
|
QString version() const;
|
2021-03-24 19:04:03 +00:00
|
|
|
EventBus *eventbus() const { return m_eventbus; }
|
2021-02-14 12:29:30 +00:00
|
|
|
WebSocket *websocket() const { return m_webSocket; }
|
2021-03-25 16:32:00 +00:00
|
|
|
bool online() const { return m_online; }
|
2020-10-08 01:00:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @brief Sets the error handler of a reply to this classes default error handler
|
|
|
|
* @param rep The reply to set the error handler on.
|
|
|
|
*
|
|
|
|
* Motivation for this helper is because I forget the correct signature each time, with all the
|
|
|
|
* funky casts.
|
|
|
|
*/
|
|
|
|
void setDefaultErrorHandler(QNetworkReply *rep) {
|
2021-03-24 19:04:03 +00:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
|
|
connect(rep, &QNetworkReply::errorOccurred, this, &ApiClient::defaultNetworkErrorHandler);
|
|
|
|
#else
|
2020-10-08 01:00:08 +00:00
|
|
|
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
|
|
|
this, &ApiClient::defaultNetworkErrorHandler);
|
2021-03-24 19:04:03 +00:00
|
|
|
#endif
|
2020-10-08 01:00:08 +00:00
|
|
|
}
|
2020-09-15 14:53:13 +00:00
|
|
|
signals:
|
|
|
|
/*
|
|
|
|
* Emitted when the server requires authentication. Please authenticate your user via authenticate.
|
|
|
|
*/
|
|
|
|
void authenticationRequired();
|
|
|
|
|
|
|
|
void authenticationError(ApiError error);
|
|
|
|
|
|
|
|
void connectionFailed(ApiError error);
|
|
|
|
void connectionSuccess(QString loginMessage);
|
|
|
|
void networkError(QNetworkReply::NetworkError error);
|
|
|
|
|
|
|
|
void authenticatedChanged(bool authenticated);
|
|
|
|
void baseUrlChanged(const QString &baseUrl);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @brief Set-up is required. You'll need to manually set up the baseUrl-property, call setupConnection
|
|
|
|
* afterwards and finally call authenticate.
|
|
|
|
*/
|
|
|
|
void setupRequired();
|
|
|
|
|
|
|
|
void userIdChanged(QString userId);
|
|
|
|
|
2021-02-13 20:42:57 +00:00
|
|
|
void deviceProfileChanged();
|
2021-03-24 19:04:03 +00:00
|
|
|
|
|
|
|
void supportedCommandsChanged();
|
2021-03-25 16:32:00 +00:00
|
|
|
void onlineChanged();
|
2021-02-13 20:42:57 +00:00
|
|
|
|
2020-10-09 00:33:08 +00:00
|
|
|
/**
|
|
|
|
* @brief onUserDataChanged Emitted when the user data of an item is changed on the server.
|
|
|
|
* @param itemId The id of the item being changed
|
|
|
|
* @param userData The new user data
|
|
|
|
*
|
|
|
|
* Note: only Jellyfin::UserData should connect to this signal, they will update themselves!
|
2021-02-20 22:20:39 +00:00
|
|
|
* Note: the userData is only valid during this callback, afterwards it is deleted!
|
2020-10-09 00:33:08 +00:00
|
|
|
*/
|
2021-02-20 22:20:39 +00:00
|
|
|
void userDataChanged(const QString &itemId, UserData *userData);
|
2020-10-09 00:33:08 +00:00
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
public slots:
|
|
|
|
/**
|
|
|
|
* @brief Tries to access credentials and connect to a server. If nothing has been configured yet,
|
|
|
|
* emits setupRequired();
|
|
|
|
*/
|
2020-09-25 12:46:39 +00:00
|
|
|
void restoreSavedSession();
|
2020-09-15 14:53:13 +00:00
|
|
|
/*
|
|
|
|
* Try to connect with the server. Tries to resolve redirects and retrieves information
|
|
|
|
* about the login procedure. Emits connectionSuccess on success, networkError or ConnectionFailed
|
|
|
|
* otherwise.
|
|
|
|
*/
|
|
|
|
void setupConnection();
|
|
|
|
void authenticate(QString username, QString password, bool storeCredentials = false);
|
2020-09-26 21:29:45 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @brief Logs the user out and clears the session.
|
|
|
|
*/
|
|
|
|
void deleteSession();
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/**
|
|
|
|
* @brief Shares the capabilities of this device to the server.
|
|
|
|
*/
|
|
|
|
void postCapabilities();
|
2020-10-10 15:28:13 +00:00
|
|
|
QString downloadUrl(const QString &itemId) const;
|
2020-09-25 12:46:39 +00:00
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
protected slots:
|
|
|
|
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
2021-02-20 22:20:39 +00:00
|
|
|
void onUserDataChanged(const QString &itemId, UserData *newData);
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
protected:
|
|
|
|
/**
|
|
|
|
* @brief Adds default headers to each request, like authentication headers etc.
|
|
|
|
* @param request The request to add headers to
|
|
|
|
* @param path The path to which the request is being made
|
|
|
|
*/
|
|
|
|
void addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery ¶ms = QUrlQuery());
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/**
|
|
|
|
* @brief Adds the authorization to the header
|
|
|
|
* @param The request to add the header to
|
|
|
|
*/
|
|
|
|
void addTokenHeader(QNetworkRequest &request);
|
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
/**
|
|
|
|
* @brief getBrandingConfiguration Gets the login message and custom CSS (which we ignore)
|
|
|
|
*/
|
|
|
|
void getBrandingConfiguration();
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/**
|
|
|
|
* @brief Generates a profile, containing the name of the application, manufacturer and most importantly,
|
|
|
|
* which media types this device supports.
|
|
|
|
*
|
|
|
|
* The actual detection of supported media types is done within jellyfindeviceprofile.cpp, since the code
|
|
|
|
* is a big mess and should be safely contained in it's own file.
|
|
|
|
*/
|
|
|
|
void generateDeviceProfile();
|
2020-09-26 21:29:45 +00:00
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
private:
|
2020-09-25 12:46:39 +00:00
|
|
|
QNetworkAccessManager m_naManager;
|
|
|
|
/*
|
|
|
|
* State information
|
|
|
|
*/
|
2020-10-02 10:20:54 +00:00
|
|
|
WebSocket *m_webSocket;
|
2021-03-24 19:04:03 +00:00
|
|
|
EventBus *m_eventbus;
|
2020-09-15 14:53:13 +00:00
|
|
|
CredentialsManager * m_credManager;
|
|
|
|
QString m_token;
|
|
|
|
QString m_deviceName;
|
|
|
|
QString m_deviceId;
|
|
|
|
QString m_userId = "";
|
2020-09-25 12:46:39 +00:00
|
|
|
QJsonObject m_deviceProfile;
|
|
|
|
QJsonObject m_playbackDeviceProfile;
|
2021-03-25 16:32:00 +00:00
|
|
|
bool m_online = true;
|
2021-03-26 20:27:35 +00:00
|
|
|
QVariantList m_supportedCommands;
|
2020-09-25 12:46:39 +00:00
|
|
|
|
|
|
|
bool m_authenticated = false;
|
|
|
|
/**
|
|
|
|
* @brief The base url of the request.
|
|
|
|
*/
|
|
|
|
QString m_baseUrl;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Setters
|
|
|
|
*/
|
2020-09-15 14:53:13 +00:00
|
|
|
|
2020-10-02 10:20:54 +00:00
|
|
|
void setAuthenticated(bool authenticated);
|
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
void setUserId(QString userId) {
|
|
|
|
this->m_userId = userId;
|
|
|
|
emit userIdChanged(userId);
|
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/*
|
|
|
|
* Utilities
|
|
|
|
*/
|
2020-09-15 14:53:13 +00:00
|
|
|
|
2021-03-24 19:04:03 +00:00
|
|
|
/**
|
|
|
|
* @brief Retreives the device ID or generates a random one.
|
|
|
|
*/
|
|
|
|
QUuid retrieveDeviceId();
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
/**
|
|
|
|
* @brief Returns the statusCode of a QNetworkReply
|
|
|
|
* @param The reply to obtain the statusCode of
|
|
|
|
* @return The statuscode of the reply
|
|
|
|
*
|
2021-03-24 19:04:03 +00:00
|
|
|
* Seriously, Qt, why is your method to obtain the status code of a request so tedious?
|
|
|
|
* It could've just been a rep->statusCode();
|
2020-09-25 12:46:39 +00:00
|
|
|
*/
|
2020-09-15 14:53:13 +00:00
|
|
|
static inline int statusCode(QNetworkReply *rep) {
|
|
|
|
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
}
|
2020-09-26 21:29:45 +00:00
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
};
|
2020-09-25 12:46:39 +00:00
|
|
|
} // NS Jellyfin
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
#endif // JELLYFIN_API_CLIENT
|