From 97429ff6ec43752bc6640cef9d763531a077819e Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Fri, 2 Oct 2020 12:20:54 +0200 Subject: [PATCH] Added basis for WebSocket connection --- harbour-sailfin.pro | 4 +- rpm/harbour-sailfin.yaml | 2 + src/jellyfinapiclient.cpp | 9 +++- src/jellyfinapiclient.h | 10 ++-- src/jellyfinwebsocket.cpp | 108 ++++++++++++++++++++++++++++++++++++++ src/jellyfinwebsocket.h | 76 +++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 src/jellyfinwebsocket.cpp create mode 100644 src/jellyfinwebsocket.h diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index b80989c..dfd9d36 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -12,7 +12,7 @@ # The name of your application TARGET = harbour-sailfin -QT += multimedia +QT += multimedia websockets CONFIG += sailfishapp c++11 @@ -31,6 +31,7 @@ SOURCES += \ src/jellyfinapimodel.cpp \ src/jellyfindeviceprofile.cpp \ src/jellyfinmediasource.cpp \ + src/jellyfinwebsocket.cpp \ src/serverdiscoverymodel.cpp DISTFILES += \ @@ -90,4 +91,5 @@ HEADERS += \ src/jellyfinapimodel.h \ src/jellyfindeviceprofile.h \ src/jellyfinmediasource.h \ + src/jellyfinwebsocket.h \ src/serverdiscoverymodel.h diff --git a/rpm/harbour-sailfin.yaml b/rpm/harbour-sailfin.yaml index 034f71b..bd69e09 100644 --- a/rpm/harbour-sailfin.yaml +++ b/rpm/harbour-sailfin.yaml @@ -22,6 +22,7 @@ PkgConfigBR: - Qt5Core - Qt5Qml - Qt5Quick + - Qt5WebSockets # Build dependencies without a pkgconfig setup can be listed here # PkgBR: @@ -30,6 +31,7 @@ PkgConfigBR: # Runtime dependencies which are not automatically detected Requires: - sailfishsilica-qt5 >= 0.10.9 + - qt5-qtdeclarative-import-xmllistmodel # All installed files Files: diff --git a/src/jellyfinapiclient.cpp b/src/jellyfinapiclient.cpp index 83d1d4d..f8a339a 100644 --- a/src/jellyfinapiclient.cpp +++ b/src/jellyfinapiclient.cpp @@ -21,7 +21,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA namespace Jellyfin { ApiClient::ApiClient(QObject *parent) - : QObject(parent) { + : QObject(parent), + m_webSocket(new WebSocket(this)) { m_deviceName = QHostInfo::localHostName(); m_deviceId = QUuid::createUuid().toString(); // TODO: make this not random? m_credManager = CredentialsManager::newInstance(this); @@ -268,4 +269,10 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) { } rep->deleteLater(); } + +void ApiClient::setAuthenticated(bool authenticated) { + this->m_authenticated = authenticated; + if (authenticated) m_webSocket->open(); + emit authenticatedChanged(authenticated); +} } diff --git a/src/jellyfinapiclient.h b/src/jellyfinapiclient.h index d60cda2..c1df53f 100644 --- a/src/jellyfinapiclient.h +++ b/src/jellyfinapiclient.h @@ -37,9 +37,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "credentialmanager.h" #include "jellyfindeviceprofile.h" +#include "jellyfinwebsocket.h" namespace Jellyfin { class MediaSource; +class WebSocket; /** * @brief An Api client for Jellyfin. Handles requests and authentication. * @@ -67,6 +69,7 @@ class MediaSource; */ class ApiClient : public QObject { friend class MediaSource; + friend class WebSocket; Q_OBJECT public: explicit ApiClient(QObject *parent = nullptr); @@ -194,6 +197,7 @@ private: /* * State information */ + WebSocket *m_webSocket; CredentialsManager * m_credManager; QString m_token; QString m_deviceName; @@ -212,10 +216,8 @@ private: * Setters */ - void setAuthenticated(bool authenticated) { - this->m_authenticated = authenticated; - emit authenticatedChanged(authenticated); - } + void setAuthenticated(bool authenticated); + void setUserId(QString userId) { this->m_userId = userId; emit userIdChanged(userId); diff --git a/src/jellyfinwebsocket.cpp b/src/jellyfinwebsocket.cpp new file mode 100644 index 0000000..354ffc6 --- /dev/null +++ b/src/jellyfinwebsocket.cpp @@ -0,0 +1,108 @@ +/* +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 +*/ +#include "jellyfinwebsocket.h" + +namespace Jellyfin { +WebSocket::WebSocket(ApiClient *client) + : QObject (client), m_apiClient(client){ + connect(&m_webSocket, &QWebSocket::connected, this, &WebSocket::onConnected); + connect(&m_webSocket, static_cast(&QWebSocket::error), + this, [this](QAbstractSocket::SocketError error) { + Q_UNUSED(error) + qDebug() << "Connection error: " << m_webSocket.errorString(); + }); +} + +void WebSocket::open() { + QUrlQuery query; + query.addQueryItem("api_key", m_apiClient->token()); + query.addQueryItem("deviceId", m_apiClient->m_deviceId); + QUrl connectionUrl(m_apiClient->baseUrl()); + connectionUrl.setScheme(connectionUrl.scheme() == "http" ? "ws" : "wss"); + connectionUrl.setPath("/socket"); + connectionUrl.setQuery(query); + m_webSocket.open(connectionUrl); + qDebug() << "Opening WebSocket connection to " << m_webSocket.requestUrl(); +} + +void WebSocket::onConnected() { + connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived); +} + +void WebSocket::textMessageReceived(const QString &message) { + QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + qWarning() << "Malformed message received over WebSocket: parse error or root not an object."; + return; + } + QJsonObject messageRoot = doc.object(); + if (!messageRoot.contains("MessageType") || !messageRoot.contains("Data")) { + qWarning() << "Malformed message received over WebSocket: no MessageType and Data set."; + return; + } + + // Convert the type so we can use it in our enums. + QString messageTypeStr = messageRoot["MessageType"].toString(); + bool ok; + MessageType messageType = static_cast(QMetaEnum::fromType().keyToValue(messageTypeStr.toLatin1(), &ok)); + if (!ok) { + qWarning() << "Unknown message arrived: " << messageTypeStr; + return; + } + + QJsonValue data = messageRoot["Data"]; + qDebug() << "Received message: " << messageTypeStr; + + switch (messageType) { + case ForceKeepAlive: + setupKeepAlive(data.toInt(-1)); + break; + case KeepAlive: + //TODO: do something? + break; + } + +} + +void WebSocket::sendKeepAlive() { + sendMessage(KeepAlive); +} + +void WebSocket::setupKeepAlive(int data) { + // Data is timeout in seconds, we want to send a keepalive at half the timeout + m_keepAliveTimer.setInterval(data * 500); + m_keepAliveTimer.setSingleShot(false); + connect(&m_keepAliveTimer, &QTimer::timeout, this, &WebSocket::sendKeepAlive); + m_keepAliveTimer.start(); + sendKeepAlive(); +} + +QString WebSocket::generateMessageId() { + return QUuid::createUuid().toString(); +} + +void WebSocket::sendMessage(MessageType type, QJsonValue data) { + QJsonObject root; + root["MessageType"] = QVariant::fromValue(type).toString(); + root["Data"] = data; + QString message = QJsonDocument(root).toJson(QJsonDocument::Compact); + m_webSocket.sendTextMessage(message); + qDebug() << "Sent message: " << message; +} +} diff --git a/src/jellyfinwebsocket.h b/src/jellyfinwebsocket.h new file mode 100644 index 0000000..3b9804c --- /dev/null +++ b/src/jellyfinwebsocket.h @@ -0,0 +1,76 @@ +/* +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 +*/ +#ifndef JELLYFIN_WEBSOCKET_H +#define JELLYFIN_WEBSOCKET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "jellyfinapiclient.h" + +namespace Jellyfin { +class ApiClient; +class WebSocket : public QObject { + Q_OBJECT +public: + /** + * @brief WebSocket creates a webSocket for a Jellyfin server to handle real time updates. + * @param client The client to create the socket for. + * + * The socket will automatically set the ApiClient to its parent. + */ + explicit WebSocket(ApiClient *client); + enum MessageType { + ForceKeepAlive, + KeepAlive + }; + Q_ENUM(MessageType) +public slots: + void open(); +private slots: + void textMessageReceived(const QString &message); + void onConnected(); + + void sendKeepAlive(); +signals: + void commandReceived(QString arts, QVariantMap args); + +protected: + ApiClient *m_apiClient; + QWebSocket m_webSocket; + + QTimer m_keepAliveTimer; + + + void setupKeepAlive(int data); + void sendMessage(MessageType type, QJsonValue data = QJsonValue()); + QString generateMessageId(); +}; +} + +#endif // JELLYFIN_WEBSOCKET_H