mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 17:25:17 +00:00
Chris Josten
9266f65c2f
Device icons for the local device is now determined by looking at what the value of the deviceType property of the ApiClient is. This property was newly introduced, so that applications using JellyfinQt can set their own device type. For other devices, a guess is made based on the client name. This guess has been derived from what Jellyfin Web does.
315 lines
12 KiB
C++
315 lines
12 KiB
C++
/*
|
|
Sailfin: a Jellyfin client written using Qt
|
|
Copyright (C) 2021 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
|
|
*/
|
|
|
|
#ifndef JELLYFIN_API_CLIENT
|
|
#define JELLYFIN_API_CLIENT
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QJsonValue>
|
|
|
|
#include <QHostInfo>
|
|
#include <QObject>
|
|
#include <QQmlListProperty>
|
|
#include <QQmlParserStatus>
|
|
#include <QScopedPointer>
|
|
#include <QString>
|
|
#include <QSysInfo>
|
|
#include <QtQml>
|
|
#include <QUuid>
|
|
|
|
#include <QNetworkReply>
|
|
#include <QUrlQuery>
|
|
|
|
#include "dto/generalcommandtype.h"
|
|
#include "credentialmanager.h"
|
|
#include "model/controllablesession.h"
|
|
#include "model/deviceprofile.h"
|
|
#include "eventbus.h"
|
|
|
|
namespace Jellyfin {
|
|
class PlaybackManager;
|
|
class WebSocket;
|
|
|
|
namespace ViewModel {
|
|
class Settings;
|
|
}
|
|
|
|
namespace DTO {
|
|
class UserItemDataDto; // Keep it as an opaque pointer
|
|
using UserData = UserItemDataDto;
|
|
}
|
|
|
|
using namespace DTO;
|
|
|
|
class ApiClientPrivate;
|
|
|
|
/**
|
|
* @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, public QQmlParserStatus {
|
|
friend class WebSocket;
|
|
friend class PlaybackManager;
|
|
Q_OBJECT
|
|
Q_INTERFACES(QQmlParserStatus)
|
|
Q_DECLARE_PRIVATE(ApiClient);
|
|
public:
|
|
explicit ApiClient(QObject *parent = nullptr);
|
|
virtual ~ApiClient();
|
|
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
|
|
Q_PROPERTY(QString appName READ appName WRITE setAppName NOTIFY appNameChanged)
|
|
Q_PROPERTY(Jellyfin::Model::DeviceTypeClass::Value deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged)
|
|
Q_PROPERTY(bool authenticated READ authenticated WRITE setAuthenticated NOTIFY authenticatedChanged)
|
|
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
|
Q_PROPERTY(QJsonObject deviceProfile READ deviceProfileJson NOTIFY deviceProfileChanged)
|
|
Q_PROPERTY(QString version READ version)
|
|
Q_PROPERTY(Jellyfin::EventBus *eventbus READ eventbus FINAL)
|
|
Q_PROPERTY(Jellyfin::WebSocket *websocket READ websocket FINAL)
|
|
Q_PROPERTY(QVariantList supportedCommands READ supportedCommands WRITE setSupportedCommands NOTIFY supportedCommandsChanged)
|
|
Q_PROPERTY(Jellyfin::ViewModel::Settings *settings READ settings NOTIFY settingsChanged)
|
|
/**
|
|
* 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)
|
|
|
|
bool authenticated() const;
|
|
void setBaseUrl(const QString &url);
|
|
void setAppName(const QString &appName);
|
|
void setDeviceType(Model::DeviceType deviceType);
|
|
|
|
QNetworkReply *get(const QString &path, const QUrlQuery ¶ms = QUrlQuery());
|
|
QNetworkReply *post(const QString &path, const QJsonDocument &data, const QUrlQuery ¶ms = QUrlQuery());
|
|
QNetworkReply *post(const QString &path, const QByteArray &data = QByteArray(), const QUrlQuery ¶ms = QUrlQuery());
|
|
|
|
enum ApiError {
|
|
JSON_ERROR,
|
|
UNEXPECTED_REPLY,
|
|
UNEXPECTED_STATUS,
|
|
INVALID_PASSWORD
|
|
};
|
|
Q_ENUM(ApiError)
|
|
|
|
const QString &baseUrl() const;
|
|
const QString &appName() const;
|
|
const QString &userId() const;
|
|
const QString &deviceId() const;
|
|
Model::DeviceType deviceType() const;
|
|
/**
|
|
* @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.
|
|
*/
|
|
QVariantList supportedCommands() const ;
|
|
void setSupportedCommands(QVariantList newSupportedCommands);
|
|
const QJsonObject deviceProfileJson() const;
|
|
QSharedPointer<DTO::DeviceProfile> deviceProfile() const;
|
|
const QJsonObject clientCapabilities() const;
|
|
/**
|
|
* @brief Retrieves the authentication token. Null QString if not authenticated.
|
|
* @note This is not the full authentication header, just the token.
|
|
*/
|
|
const QString &token() const;
|
|
QString version() const;
|
|
bool online() const;
|
|
|
|
EventBus *eventbus() const;
|
|
WebSocket *websocket() const;
|
|
ViewModel::Settings * settings() const;
|
|
|
|
/**
|
|
* @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) {
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
connect(rep, &QNetworkReply::errorOccurred, this, &ApiClient::defaultNetworkErrorHandler);
|
|
#else
|
|
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
|
this, &ApiClient::defaultNetworkErrorHandler);
|
|
#endif
|
|
}
|
|
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);
|
|
void appNameChanged(const QString &newAppName);
|
|
void settingsChanged();
|
|
|
|
/**
|
|
* @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);
|
|
|
|
void deviceProfileChanged();
|
|
void deviceTypeChanged();
|
|
|
|
void supportedCommandsChanged();
|
|
void onlineChanged();
|
|
|
|
/**
|
|
* @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!
|
|
* Note: the userData is only valid during this callback, afterwards it is deleted!
|
|
*/
|
|
void userDataChanged(const QString &itemId, UserData *userData);
|
|
|
|
public slots:
|
|
/**
|
|
* @brief Tries to access credentials and connect to a server. If nothing has been configured yet,
|
|
* emits setupRequired();
|
|
*/
|
|
void restoreSavedSession();
|
|
/*
|
|
* 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);
|
|
|
|
/**
|
|
* @brief Logs the user out and clears the session.
|
|
*/
|
|
void deleteSession();
|
|
|
|
/**
|
|
* @brief Shares the capabilities of this device to the server.
|
|
*/
|
|
void postCapabilities();
|
|
QString downloadUrl(const QString &itemId) const;
|
|
|
|
protected slots:
|
|
void defaultNetworkErrorHandler(QNetworkReply::NetworkError error);
|
|
void onUserDataChanged(const QString &itemId, UserData *newData);
|
|
void credManagerServersListed(QStringList users);
|
|
void credManagerUsersListed(const QString &server, QStringList users);
|
|
void credManagerTokenRetrieved(const QString &server, const QString &user, const QString &token);
|
|
|
|
void classBegin() override;
|
|
void componentComplete() override;
|
|
|
|
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()) const;
|
|
|
|
/**
|
|
* @brief Adds the authorization to the header
|
|
* @param The request to add the header to
|
|
*/
|
|
void addTokenHeader(QNetworkRequest &request) const;
|
|
|
|
/**
|
|
* @brief getBrandingConfiguration Gets the login message and custom CSS (which we ignore)
|
|
*/
|
|
void getBrandingConfiguration();
|
|
|
|
/**
|
|
* @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();
|
|
|
|
|
|
private:
|
|
QScopedPointer<ApiClientPrivate> d_ptr;
|
|
QNetworkAccessManager m_naManager;
|
|
/*
|
|
* State information
|
|
*/
|
|
/*
|
|
* Setters
|
|
*/
|
|
void setAuthenticated(bool authenticated);
|
|
void setUserId(const QString &userId);
|
|
|
|
/**
|
|
* @brief Returns the statusCode of a QNetworkReply
|
|
* @param The reply to obtain the statusCode of
|
|
* @return The statuscode of the reply
|
|
*
|
|
* Seriously, Qt, why is your method to obtain the status code of a request so tedious?
|
|
* It could've just been a rep->statusCode();
|
|
*/
|
|
static inline int statusCode(QNetworkReply *rep) {
|
|
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
}
|
|
|
|
};
|
|
} // NS Jellyfin
|
|
|
|
#endif // JELLYFIN_API_CLIENT
|