2020-09-27 18:38:33 +00:00
|
|
|
/*
|
|
|
|
Sailfin: a Jellyfin client written using Qt
|
|
|
|
Copyright (C) 2020 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
|
|
|
|
*/
|
|
|
|
|
2020-09-15 14:53:13 +00:00
|
|
|
#include "jellyfinapiclient.h"
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
namespace Jellyfin {
|
|
|
|
ApiClient::ApiClient(QObject *parent)
|
2020-09-15 14:53:13 +00:00
|
|
|
: QObject(parent) {
|
|
|
|
m_deviceName = QHostInfo::localHostName();
|
2020-09-25 12:46:39 +00:00
|
|
|
m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random?
|
2020-09-27 01:14:05 +00:00
|
|
|
m_credManager = CredentialsManager::newInstance(this);
|
2020-09-25 12:46:39 +00:00
|
|
|
|
|
|
|
generateDeviceProfile();
|
2020-09-15 14:53:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// BASE HTTP METHODS //
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery ¶ms) {
|
|
|
|
addTokenHeader(request);
|
|
|
|
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
|
2020-09-26 02:13:21 +00:00
|
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(m_version));
|
2020-09-25 12:46:39 +00:00
|
|
|
QString url = this->m_baseUrl + path;
|
|
|
|
if (!params.isEmpty()) url += "?" + params.toString();
|
|
|
|
request.setUrl(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ApiClient::addTokenHeader(QNetworkRequest &request) {
|
2020-09-26 02:13:21 +00:00
|
|
|
QString authentication = "MediaBrowser ";
|
2020-09-15 14:53:13 +00:00
|
|
|
authentication += "Client=\"Sailfin\"";
|
|
|
|
authentication += ", Device=\"" + m_deviceName + "\"";
|
|
|
|
authentication += ", DeviceId=\"" + m_deviceId + "\"";
|
2020-09-26 02:13:21 +00:00
|
|
|
authentication += ", Version=\"" + QString(m_version) + "\"";
|
2020-09-15 14:53:13 +00:00
|
|
|
if (m_authenticated) {
|
|
|
|
authentication += ", token=\"" + m_token + "\"";
|
|
|
|
}
|
|
|
|
request.setRawHeader("X-Emby-Authorization", authentication.toUtf8());
|
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
QNetworkReply *ApiClient::get(const QString &path, const QUrlQuery ¶ms) {
|
2020-09-15 14:53:13 +00:00
|
|
|
QNetworkRequest req;
|
|
|
|
addBaseRequestHeaders(req, path, params);
|
2020-09-25 12:46:39 +00:00
|
|
|
qDebug() << "GET " << req.url();
|
2020-09-15 14:53:13 +00:00
|
|
|
return m_naManager.get(req);
|
|
|
|
}
|
2020-09-25 12:46:39 +00:00
|
|
|
QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, const QUrlQuery ¶ms) {
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
QNetworkRequest req;
|
|
|
|
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
2020-09-25 12:46:39 +00:00
|
|
|
addBaseRequestHeaders(req, path, params);
|
|
|
|
qDebug() << "POST " << req.url();
|
2020-09-15 14:53:13 +00:00
|
|
|
if (data.isEmpty())
|
|
|
|
return m_naManager.post(req, QByteArray());
|
|
|
|
else {
|
|
|
|
return m_naManager.post(req, data.toJson(QJsonDocument::Compact));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Nice to have methods //
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::restoreSavedSession(){
|
2020-09-15 14:53:13 +00:00
|
|
|
QObject *ctx1 = new QObject(this);
|
|
|
|
connect(m_credManager, &CredentialsManager::serversListed, ctx1, [this, ctx1](const QStringList &servers) {
|
|
|
|
qDebug() << "Servers listed: " << servers;
|
|
|
|
if (servers.size() == 0) {
|
|
|
|
emit this->setupRequired();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//FIXME: support multiple servers
|
|
|
|
QString server = servers[0];
|
|
|
|
this->m_baseUrl = server;
|
|
|
|
qDebug() << "Server: " << server;
|
|
|
|
QObject *ctx2 = new QObject(this);
|
|
|
|
connect(m_credManager, &CredentialsManager::usersListed, ctx2, [this, server, ctx2](const QStringList &users) {
|
|
|
|
if (users.size() == 0) {
|
|
|
|
emit this->setupRequired();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
//FIXME: support multiple users
|
|
|
|
QString user = users[0];
|
|
|
|
qDebug() << "User: " << user;
|
|
|
|
|
|
|
|
QObject *ctx3 = new QObject(this);
|
|
|
|
connect(m_credManager, &CredentialsManager::tokenRetrieved, ctx3, [this, ctx3]
|
|
|
|
(const QString &server, const QString &user, const QString &token) {
|
|
|
|
Q_UNUSED(server)
|
|
|
|
this->m_token = token;
|
|
|
|
this->setUserId(user);
|
|
|
|
this->setAuthenticated(true);
|
2020-09-25 12:46:39 +00:00
|
|
|
this->postCapabilities();
|
2020-09-15 14:53:13 +00:00
|
|
|
disconnect(ctx3);
|
|
|
|
}, Qt::UniqueConnection);
|
|
|
|
m_credManager->get(server, user);
|
|
|
|
delete ctx2;
|
|
|
|
}, Qt::UniqueConnection);
|
|
|
|
m_credManager->listUsers(server);
|
|
|
|
qDebug() << "Listing users";
|
|
|
|
delete ctx1;
|
|
|
|
}, Qt::UniqueConnection);
|
|
|
|
qDebug() << "Listing servers";
|
|
|
|
m_credManager->listServers();
|
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::setupConnection() {
|
2020-09-15 14:53:13 +00:00
|
|
|
// First detect redirects:
|
|
|
|
// Note that this is done without calling JellyfinApiClient::get since that automatically includes the base_url,
|
|
|
|
// which is something we want to avoid here.
|
|
|
|
QNetworkReply *rep = m_naManager.get(QNetworkRequest(m_baseUrl));
|
|
|
|
connect(rep, &QNetworkReply::finished, this, [rep, this](){
|
|
|
|
int status = statusCode(rep);
|
|
|
|
qDebug() << status;
|
|
|
|
|
|
|
|
// Check if redirect
|
|
|
|
if (status >= 300 && status < 400) {
|
|
|
|
QString location = QString::fromUtf8(rep->rawHeader("location"));
|
|
|
|
qInfo() << "Redirect from " << this->m_baseUrl << " to " << location;
|
|
|
|
QUrl base = QUrl(m_baseUrl);
|
|
|
|
QString newUrl = base.resolved(QUrl(location)).toString();
|
|
|
|
// If the url wants to redirect us to their web interface, we have to chop the last part of.
|
|
|
|
if (newUrl.endsWith("/web/index.html")) {
|
|
|
|
newUrl.chop(QString("/web/index.html").size());
|
|
|
|
this->setBaseUrl(newUrl);
|
|
|
|
getBrandingConfiguration();
|
|
|
|
} else {
|
|
|
|
this->setBaseUrl(newUrl);
|
|
|
|
setupConnection();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
getBrandingConfiguration();
|
|
|
|
}
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-26 21:29:45 +00:00
|
|
|
setDefaultErrorHandler(rep);
|
2020-09-15 14:53:13 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::getBrandingConfiguration() {
|
2020-09-15 14:53:13 +00:00
|
|
|
QNetworkReply *rep = get("/Branding/Configuration");
|
|
|
|
connect(rep, &QNetworkReply::finished, this, [rep, this]() {
|
|
|
|
qDebug() << "RESPONSE: " << statusCode(rep);
|
|
|
|
switch(statusCode(rep)) {
|
|
|
|
case 200:
|
|
|
|
QJsonDocument response = QJsonDocument::fromJson(rep->readAll());
|
|
|
|
if (response.isNull() || !response.isObject()) {
|
|
|
|
emit this->connectionFailed(ApiError::JSON_ERROR);
|
|
|
|
} else {
|
|
|
|
QJsonObject obj = response.object();
|
|
|
|
if (obj.contains("LoginDisclaimer")) {
|
|
|
|
qDebug() << "Login disclaimer: " << obj["LoginDisclaimer"];
|
|
|
|
emit this->connectionSuccess(obj["LoginDisclaimer"].toString());
|
|
|
|
} else {
|
|
|
|
emit this->connectionSuccess("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-26 21:29:45 +00:00
|
|
|
setDefaultErrorHandler(rep);
|
2020-09-15 14:53:13 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::authenticate(QString username, QString password, bool storeCredentials) {
|
2020-09-15 14:53:13 +00:00
|
|
|
QJsonObject requestData;
|
|
|
|
|
|
|
|
requestData["Username"] = username;
|
|
|
|
requestData["Pw"] = password;
|
|
|
|
QNetworkReply *rep = post("/Users/Authenticatebyname", QJsonDocument(requestData));
|
|
|
|
connect(rep, &QNetworkReply::finished, this, [rep, username, storeCredentials, this]() {
|
|
|
|
int status = rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
qDebug() << "Got reply with status code " << status;
|
|
|
|
if (status >= 200 && status < 300) {
|
|
|
|
QJsonObject authInfo = QJsonDocument::fromJson(rep->readAll()).object();
|
|
|
|
this->m_token = authInfo["AccessToken"].toString();
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
// Fool this class's addRequestheaders to add the token, without
|
|
|
|
// notifying QML that we're authenticated, to prevent other requests going first.
|
|
|
|
this->m_authenticated = true;
|
2020-09-15 14:53:13 +00:00
|
|
|
this->setUserId(authInfo["User"].toObject()["Id"].toString());
|
2020-09-25 12:46:39 +00:00
|
|
|
this->postCapabilities();
|
|
|
|
this->setAuthenticated(true);
|
2020-09-15 14:53:13 +00:00
|
|
|
|
|
|
|
if (storeCredentials) {
|
|
|
|
m_credManager->store(this->m_baseUrl, this->m_userId, this->m_token);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-26 21:29:45 +00:00
|
|
|
setDefaultErrorHandler(rep);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ApiClient::deleteSession() {
|
|
|
|
QNetworkReply *rep = post("/Sessions/Logout");
|
|
|
|
connect(rep, &QNetworkReply::finished, this, [rep, this] {
|
|
|
|
m_credManager->remove(m_baseUrl, m_userId);
|
|
|
|
this->setAuthenticated(false);
|
|
|
|
emit this->setupRequired();
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-15 14:53:13 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::fetchItem(const QString &id) {
|
2020-09-15 14:53:13 +00:00
|
|
|
QNetworkReply *rep = get("/Users/" + m_userId + "/Items/" + id);
|
|
|
|
connect(rep, &QNetworkReply::finished, this, [rep, id, this]() {
|
|
|
|
int status = statusCode(rep);
|
|
|
|
if (status >= 200 && status < 300) {
|
|
|
|
QJsonObject data = QJsonDocument::fromJson(rep->readAll()).object();
|
|
|
|
emit this->itemFetched(id, data);
|
|
|
|
}
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-26 21:29:45 +00:00
|
|
|
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
|
|
|
this, [id, rep, this](QNetworkReply::NetworkError error) {
|
|
|
|
emit this->itemFetchFailed(id, error);
|
|
|
|
rep->deleteLater();
|
|
|
|
});
|
2020-09-15 14:53:13 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 12:46:39 +00:00
|
|
|
void ApiClient::postCapabilities() {
|
|
|
|
QJsonObject capabilities;
|
|
|
|
capabilities["SupportsPersistentIdentifier"] = false; // Technically untrue, but not implemented yet.
|
|
|
|
capabilities["SupportsMediaControl"] = false;
|
|
|
|
capabilities["SupportsSync"] = false;
|
|
|
|
capabilities["SupportsContentUploading"] = false;
|
|
|
|
capabilities["AppStoreUrl"] = "https://chris.netsoj.nl/projects/harbour-sailfin";
|
|
|
|
capabilities["IconUrl"] = "https://chris.netsoj.nl/static/img/logo.png";
|
|
|
|
capabilities["DeviceProfile"] = m_deviceProfile;
|
|
|
|
QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities));
|
2020-09-26 21:29:45 +00:00
|
|
|
setDefaultErrorHandler(rep);
|
2020-09-25 12:46:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ApiClient::generateDeviceProfile() {
|
|
|
|
QJsonObject root = DeviceProfile::generateProfile();
|
|
|
|
m_playbackDeviceProfile = QJsonObject(root);
|
|
|
|
root["Name"] = m_deviceName;
|
|
|
|
root["Id"] = m_deviceId;
|
|
|
|
root["FriendlyName"] = QSysInfo::prettyProductName();
|
|
|
|
QJsonArray playableMediaTypes;
|
|
|
|
playableMediaTypes.append("Audio");
|
|
|
|
playableMediaTypes.append("Video");
|
|
|
|
playableMediaTypes.append("Photo");
|
|
|
|
root["PlayableMediaTypes"] = playableMediaTypes;
|
|
|
|
|
|
|
|
m_deviceProfile = root;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
|
2020-09-15 14:53:13 +00:00
|
|
|
QObject *signalSender = sender();
|
|
|
|
QNetworkReply *rep = dynamic_cast<QNetworkReply *>(signalSender);
|
|
|
|
if (rep != nullptr && statusCode(rep) == 401) {
|
2020-09-26 21:29:45 +00:00
|
|
|
this->setAuthenticated(false);
|
2020-09-15 14:53:13 +00:00
|
|
|
emit this->authenticationError(ApiError::INVALID_PASSWORD);
|
|
|
|
} else {
|
|
|
|
emit this->networkError(error);
|
|
|
|
}
|
|
|
|
rep->deleteLater();
|
|
|
|
}
|
2020-09-25 12:46:39 +00:00
|
|
|
}
|