1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-05 18:22:46 +00:00

Deserialized a list! Restructured project!

I finally got deserializing lists working. Exposing them to QML was not
a trivial task either. Note that I didn't do it the clean way. Nested
lists are not supported. But it works!

Because I got so frustarted at one point trying to implement things the
right way, I restructured the project to seperate the Sailfish code from
the Qt code and created a new, empty desktop project. The Qt code has
been transformed into a happy little library, to which the Sailfish OS
application links.

Note that QMake doesn't seem to strip the library for some reason.
This commit is contained in:
Chris Josten 2020-10-08 03:00:08 +02:00
parent 4e3395c4e5
commit 1e80ceb697
77 changed files with 507 additions and 213 deletions

View file

@ -0,0 +1,85 @@
/*
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 "credentialmanager.h"
CredentialsManager * CredentialsManager::newInstance(QObject *parent) {
return new FallbackCredentialsManager(parent);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// FallbackCredentialsManager //
////////////////////////////////////////////////////////////////////////////////////////////////////
FallbackCredentialsManager::FallbackCredentialsManager(QObject *parent)
: CredentialsManager (parent) {
m_settings.beginGroup("Credentials");
}
QString FallbackCredentialsManager::urlToGroupName(const QString &url) const {
// |'s are not allowed in URLS, but are in group names.
return QString::number(qHash(url), 16);
}
QString FallbackCredentialsManager::groupNameToUrl(const QString &group) const {
QString tmp = QString(group);
return tmp.replace('|', "/");
}
void FallbackCredentialsManager::store(const QString &server, const QString &user, const QString &token) {
m_settings.setValue(urlToGroupName(server) + "/users/" + user + "/accessToken", token);
m_settings.setValue(urlToGroupName(server) + "/address", server);
}
void FallbackCredentialsManager::get(const QString &server, const QString &user) const {
QString result = m_settings.value(urlToGroupName(server) + "/users/" + user + "/accessToken").toString();
emit CredentialsManager::tokenRetrieved(server, user, result);
}
void FallbackCredentialsManager::remove(const QString &server, const QString &user) {
m_settings.remove(urlToGroupName(server) + "/users/" + user);
// Check if only the /address key is left. In this case, the server section should be removed.
m_settings.beginGroup(urlToGroupName(server));
int childGroupsCount = m_settings.childGroups().count();
m_settings.endGroup();
if (childGroupsCount == 0) {
m_settings.remove(urlToGroupName(server));
}
}
void FallbackCredentialsManager::listServers() const {
QList<QString> keys = m_settings.childGroups();
qDebug() << "Servers: " << keys;
for (int i = 0; i < keys.size(); i++) {
keys[i] = m_settings.value(keys[i] + "/address").toString();
}
qDebug() << "Servers: " << keys;
emit CredentialsManager::serversListed(keys);
}
void FallbackCredentialsManager::listUsers(const QString &server) {
m_settings.beginGroup(urlToGroupName(server));
m_settings.beginGroup("users");
QStringList users = m_settings.childGroups();
qDebug() << "Users: " << users;
m_settings.endGroup();
m_settings.endGroup();
emit CredentialsManager::usersListed(users);
}

19
core/src/jellyfin.cpp Normal file
View file

@ -0,0 +1,19 @@
#include "jellyfin.h"
namespace Jellyfin {
void registerTypes() {
const char* QML_NAMESPACE = "nl.netsoj.chris.Jellyfin";
// Singletons are perhaps bad, but they are convenient :)
qmlRegisterSingletonType<Jellyfin::ApiClient>(QML_NAMESPACE, 1, 0, "ApiClient", [](QQmlEngine *eng, QJSEngine *js) {
Q_UNUSED(eng)
Q_UNUSED(js)
return dynamic_cast<QObject*>(new Jellyfin::ApiClient());
});
qmlRegisterType<Jellyfin::ServerDiscoveryModel>(QML_NAMESPACE, 1, 0, "ServerDiscoveryModel");
qmlRegisterType<Jellyfin::PlaybackManager>(QML_NAMESPACE, 1, 0, "PlaybackManager");
// API models
Jellyfin::registerModels(QML_NAMESPACE);
Jellyfin::registerSerializableJsonTypes(QML_NAMESPACE);
}
}

View file

@ -0,0 +1,283 @@
/*
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 "jellyfinapiclient.h"
namespace Jellyfin {
ApiClient::ApiClient(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);
generateDeviceProfile();
}
QString ApiClient::version() const {
return QString(SAILFIN_VERSION);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// BASE HTTP METHODS //
////////////////////////////////////////////////////////////////////////////////////////////////////
void ApiClient::addBaseRequestHeaders(QNetworkRequest &request, const QString &path, const QUrlQuery &params) {
addTokenHeader(request);
request.setRawHeader("Accept", "application/json;"); // profile=\"CamelCase\"");
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Sailfin/%1").arg(version()));
QString url = this->m_baseUrl + path;
if (!params.isEmpty()) url += "?" + params.toString();
request.setUrl(url);
}
void ApiClient::addTokenHeader(QNetworkRequest &request) {
QString authentication = "MediaBrowser ";
authentication += "Client=\"Sailfin\"";
authentication += ", Device=\"" + m_deviceName + "\"";
authentication += ", DeviceId=\"" + m_deviceId + "\"";
authentication += ", Version=\"" + version() + "\"";
if (m_authenticated) {
authentication += ", token=\"" + m_token + "\"";
}
request.setRawHeader("X-Emby-Authorization", authentication.toUtf8());
}
QNetworkReply *ApiClient::get(const QString &path, const QUrlQuery &params) {
QNetworkRequest req;
addBaseRequestHeaders(req, path, params);
qDebug() << "GET " << req.url();
return m_naManager.get(req);
}
QNetworkReply *ApiClient::post(const QString &path, const QJsonDocument &data, const QUrlQuery &params) {
QNetworkRequest req;
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
addBaseRequestHeaders(req, path, params);
qDebug() << "POST " << req.url();
if (data.isEmpty())
return m_naManager.post(req, QByteArray());
else {
return m_naManager.post(req, data.toJson(QJsonDocument::Compact));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Nice to have methods //
////////////////////////////////////////////////////////////////////////////////////////////////////
void ApiClient::restoreSavedSession(){
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);
this->postCapabilities();
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();
}
void ApiClient::setupConnection() {
// 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.
QNetworkRequest req = QNetworkRequest(m_baseUrl);
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QNetworkReply *rep = m_naManager.get(req);
connect(rep, &QNetworkReply::finished, this, [rep, this](){
int status = statusCode(rep);
qDebug() << status;
QString newUrl = rep->url().toString();
// If the server wants to redirect us to their web interface, we have to chop the last part of the url off.
if (newUrl.endsWith("/web/index.html")) {
newUrl.chop(QString("/web/index.html").size());
this->setBaseUrl(newUrl);
}
this->getBrandingConfiguration();
rep->deleteLater();
});
connect(rep, &QNetworkReply::redirected, this, [req] (const QUrl &url) {
qDebug() << "Redirect from " << req.url() << " to " << url;
});
setDefaultErrorHandler(rep);
}
void ApiClient::getBrandingConfiguration() {
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();
});
setDefaultErrorHandler(rep);
}
void ApiClient::authenticate(QString username, QString password, bool storeCredentials) {
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();
// 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;
this->setUserId(authInfo["User"].toObject()["Id"].toString());
this->postCapabilities();
this->setAuthenticated(true);
if (storeCredentials) {
m_credManager->store(this->m_baseUrl, this->m_userId, this->m_token);
}
}
rep->deleteLater();
});
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();
});
}
void ApiClient::fetchItem(const QString &id) {
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();
});
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, [id, rep, this](QNetworkReply::NetworkError error) {
emit this->itemFetchFailed(id, error);
rep->deleteLater();
});
}
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));
setDefaultErrorHandler(rep);
}
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) {
QObject *signalSender = sender();
QNetworkReply *rep = dynamic_cast<QNetworkReply *>(signalSender);
if (rep != nullptr && statusCode(rep) == 401) {
this->setAuthenticated(false);
emit this->authenticationError(ApiError::INVALID_PASSWORD);
} else {
emit this->networkError(error);
}
rep->deleteLater();
}
void ApiClient::setAuthenticated(bool authenticated) {
this->m_authenticated = authenticated;
if (authenticated) m_webSocket->open();
emit authenticatedChanged(authenticated);
}
}

View file

@ -0,0 +1,223 @@
/*
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 "jellyfinapimodel.h"
namespace Jellyfin {
ApiModel::ApiModel(QString path, bool hasRecordResponse, bool addUserId, QObject *parent)
: QAbstractListModel (parent),
m_path(path),
m_hasRecordResponse(hasRecordResponse),
m_addUserId(addUserId){
}
void ApiModel::reload() {
this->setStatus(Loading);
m_startIndex = 0;
load(RELOAD);
}
void ApiModel::load(LoadType type) {
qDebug() << (type == RELOAD ? "RELOAD" : "LOAD_MORE");
if (m_apiClient == nullptr) {
qWarning() << "Please set the apiClient property before (re)loading";
return;
}
if (m_path.contains("{{user}}")) {
m_path = m_path.replace("{{user}}", m_apiClient->userId());
}
if (m_path.contains("{{show}}") && !m_show.isEmpty()) {
m_path = m_path.replace("{{show}}", m_show);
}
QUrlQuery query;
if (m_limit >= 0) {
query.addQueryItem("Limit", QString::number(m_limit));
} else {
query.addQueryItem("Limit", QString::number(DEFAULT_LIMIT));
}
if (m_startIndex > 0) {
query.addQueryItem("StartIndex", QString::number(m_startIndex));
}
if (!m_parentId.isEmpty()) {
query.addQueryItem("ParentId", m_parentId);
}
if (!m_sortBy.empty()) {
query.addQueryItem("SortBy", m_sortBy.join(","));
}
if (m_sortOrder != Unspecified) {
query.addQueryItem("SortOrder", m_sortOrder == Ascending ? "Ascending" : "Descending");
}
if (!m_imageTypes.empty()) {
query.addQueryItem("ImageTypes", m_imageTypes.join(","));
}
if (!m_fields.empty()) {
query.addQueryItem("Fields", m_fields.join(","));
}
if (!m_seasonId.isEmpty()) {
query.addQueryItem("seasonId", m_seasonId);
}
if (m_addUserId) {
query.addQueryItem("userId", m_apiClient->userId());
}
if (m_recursive) {
query.addQueryItem("Recursive", "true");
}
addQueryParameters(query);
QNetworkReply *rep = m_apiClient->get(m_path, query);
connect(rep, &QNetworkReply::finished, this, [this, type, rep]() {
QJsonDocument doc = QJsonDocument::fromJson(rep->readAll());
if (!m_hasRecordResponse) {
if (!doc.isArray()) {
qWarning() << "Object is not an array!";
this->setStatus(Error);
return;
}
this->m_array = doc.array();
} else {
if (!doc.isObject()) {
qWarning() << "Object is not an object!";
this->setStatus(Error);
return;
}
QJsonObject obj = doc.object();
if (!obj.contains("Items")) {
qWarning() << "Object doesn't contain items!";
this->setStatus(Error);
return;
}
if (m_limit < 0) {
// Javascript is beautiful
if (obj.contains("TotalRecordCount") && obj["TotalRecordCount"].isDouble()) {
m_totalRecordCount = obj["TotalRecordCount"].toInt();
m_startIndex += DEFAULT_LIMIT;
} else {
qWarning() << "Record-response does not have a total record count";
this->setStatus(Error);
return;
}
}
if (!obj["Items"].isArray()) {
qWarning() << "Items is not an array!";
this->setStatus(Error);
return;
}
QJsonArray items = obj["Items"].toArray();
switch(type) {
case RELOAD:
this->m_array = items;
break;
case LOAD_MORE:
this->beginInsertRows(QModelIndex(), m_array.size(), m_array.size() + items.size() - 1);
// QJsonArray apparently doesn't allow concatenating lists like QList or std::vector
foreach (const QJsonValue &val, items) {
m_array.append(val);
}
this->endInsertRows();
break;
}
}
if (type == RELOAD) {
generateFields();
}
this->setStatus(Ready);
rep->deleteLater();
});
}
void ApiModel::generateFields() {
if (m_array.size() == 0) return;
this->beginResetModel();
int i = Qt::UserRole + 1;
if (!m_array[0].isObject()) {
qWarning() << "Iterator is not an object?";
return;
}
// Walks over the keys in the first record and adds them to the rolenames.
// This assumes the back-end has the same keys for every record. I could technically
// go over all records to be really sure, but no-one got time for a O(n²) algorithm, so
// this heuristic hopefully suffices.
QJsonObject ob = m_array[0].toObject();
for (auto jt = ob.begin(); jt != ob.end(); jt++) {
QString keyName = jt.key();
keyName[0] = keyName[0].toLower();
QByteArray keyArr = keyName.toUtf8();
if (!m_roles.values().contains(keyArr)) {
m_roles.insert(i++, keyArr);
//qDebug() << m_path << " adding " << keyName << " as " << ( i - 1);
}
}
this->endResetModel();
}
QVariant ApiModel::data(const QModelIndex &index, int role) const {
// Ignore roles we don't know
if (role <= Qt::UserRole || role >= Qt::UserRole + m_roles.size()) return QVariant();
// Ignore invalid indices.
if (!index.isValid()) return QVariant();
QJsonObject obj = m_array.at(index.row()).toObject();
QString key = m_roles[role];
key[0] = key[0].toUpper();
if (obj.contains(key)) {
return obj[key].toVariant();
}
return QVariant();
}
bool ApiModel::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid()) return false;
switch(m_status) {
case Uninitialised:
case Loading:
return false;
default:
break;
}
if (m_limit < 0) {
return m_startIndex <= m_totalRecordCount;
} else {
return false;
}
}
void ApiModel::fetchMore(const QModelIndex &parent) {
if (parent.isValid()) return;
this->setStatus(LoadingMore);
load(LOAD_MORE);
}
void ApiModel::addQueryParameters(QUrlQuery &query) { Q_UNUSED(query)}
void registerModels(const char *URI) {
qmlRegisterUncreatableType<ApiModel>(URI, 1, 0, "ApiModel", "Is enum and base class");
qmlRegisterUncreatableType<SortOptions>(URI, 1, 0, "SortOptions", "Is enum");
qmlRegisterType<PublicUserModel>(URI, 1, 0, "PublicUserModel");
qmlRegisterType<UserViewModel>(URI, 1, 0, "UserViewModel");
qmlRegisterType<UserItemModel>(URI, 1, 0, "UserItemModel");
qmlRegisterType<UserItemLatestModel>(URI, 1, 0, "UserItemLatestModel");
qmlRegisterType<UserItemResumeModel>(URI, 1, 0, "UserItemResumeModel");
qmlRegisterType<ShowSeasonsModel>(URI, 1, 0, "ShowSeasonsModel");
qmlRegisterType<ShowEpisodesModel>(URI, 1, 0, "ShowEpisodesModel");
}
}

View file

@ -0,0 +1,191 @@
/*
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 "jellyfindeviceprofile.h"
namespace Jellyfin {
bool DeviceProfile::supportsHls() {
return true;
}
bool DeviceProfile::canPlayH264() {
return true;
}
bool DeviceProfile::canPlayAc3() {
return true;
}
bool DeviceProfile::supportsMp3VideoAudio() {
qDebug() << "Mp3VideoAudio: " << QMediaPlayer::hasSupport("video/mp4", {"avc1.640029", "mp3"}, QMediaPlayer::StreamPlayback);
return true;
}
int DeviceProfile::maxStreamingBitrate() {
return 5000000;
}
QJsonObject DeviceProfile::generateProfile() {
using JsonPair = QPair<QString, QJsonValue>;
QJsonObject profile;
QStringList videoAudioCodecs;
QStringList mp4VideoCodecs;
QStringList hlsVideoCodecs;
QStringList hlsVideoAudioCodecs;
if (canPlayH264()) {
mp4VideoCodecs.append("h264");
hlsVideoCodecs.append("h264");
}
if (canPlayAc3()) {
videoAudioCodecs.append("ac3");
hlsVideoAudioCodecs.append("ac3");
}
if (supportsMp3VideoAudio()) {
videoAudioCodecs.append("mp3");
hlsVideoAudioCodecs.append("mp3");
}
QJsonArray codecProfiles = {};
codecProfiles.append(QJsonObject {
JsonPair("Codec", "aac"),
JsonPair("Conditions", QJsonArray {
QJsonObject {
JsonPair("Property", "IsSecondaryAudio"),
JsonPair("Condition", "Equals"),
JsonPair("Value", false),
JsonPair("IsRequired", false)
}
}),
JsonPair("Type", "VideoAudio")
});
codecProfiles.append(QJsonObject {
JsonPair("Coded", "h264"),
JsonPair("Conditions", QJsonArray {
QJsonObject {
JsonPair("Property", "IsAnamorphic"),
JsonPair("Condition", "NotEquals"),
JsonPair("Value", true),
JsonPair("IsRequired", false)
},
QJsonObject {
JsonPair("Property", "VideoProfile"),
JsonPair("Condition", "EqualsAny"),
JsonPair("Value", "baseline|constrained baseline"), //"high|main|baseline|constrained baseline"),
JsonPair("IsRequired", false),
},
QJsonObject {
JsonPair("Property", "VideoLevel"),
JsonPair("Condition", "LessThanEqual"),
JsonPair("Value", 51),
JsonPair("IsRequired", false)
},
QJsonObject {
JsonPair("Property", "IsInterlaced"),
JsonPair("Condition", "NotEquals"),
JsonPair("Value", true),
JsonPair("IsRequired", false)
}
}),
JsonPair("Type", "Video")
});
QJsonArray transcodingProfiles = {};
// Hard coded nr 1:
QJsonObject transcoding1;
transcoding1["AudioCodec"] = "aac";
transcoding1["BreakOnNonKeyFrames"] =true;
transcoding1["Container"] = "ts";
transcoding1["Context"] = "Streaming";
transcoding1["MaxAudioChannels"] = 2;
transcoding1["MinSegments"] = 1;
transcoding1["Protocol"] = "hls";
transcoding1["Type"] = "Audio";
transcodingProfiles.append(transcoding1);
// Hard code nr 2
transcodingProfiles.append(QJsonObject({
JsonPair("AudioCodec", "mp3,aac"),
JsonPair("BreakOnNonKeyFrames", true),
JsonPair("Container", "ts"),
JsonPair("Context", "Streaming"),
JsonPair("MaxAudioChannels", 2),
JsonPair("MinSegments", 1),
JsonPair("Protocol", "hls"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", "h264")
}));
// Fallback
transcodingProfiles.append(QJsonObject {
JsonPair("Container", "mp4"),
JsonPair("Type", "Video"),
JsonPair("AudioCodec", videoAudioCodecs.join(',')),
JsonPair("VideoCodec", "h264"),
JsonPair("Context", "Static"),
JsonPair("Protocol", "http")
});
if (supportsHls() && !hlsVideoAudioCodecs.isEmpty()) {
transcodingProfiles.append(QJsonObject {
JsonPair("Container", "ts"),
JsonPair("Type", "Video"),
JsonPair("AudioCodec", hlsVideoAudioCodecs.join(",")),
JsonPair("VideoCodec", hlsVideoCodecs.join(",")),
JsonPair("Context", "Streaming"),
JsonPair("Protocol", "hls"),
JsonPair("MaxAudioChannels", 2),
JsonPair("MinSegments", 1),
JsonPair("BreakOnNonKeyFrames", true)
});
}
// Response profiles (or whatever it actually does?)
QJsonArray responseProfiles = {};
responseProfiles.append(QJsonObject({
JsonPair("Type", "Video"),
JsonPair("Container", "m4v"),
JsonPair("MimeType", "video/mp4")
}));
// Direct play profiles
QJsonArray directPlayProfiles;
directPlayProfiles.append(QJsonObject {
JsonPair("Container", "mp4,m4v"),
JsonPair("Type", "Video"),
JsonPair("VideoCodec", mp4VideoCodecs.join(',')),
JsonPair("AudioCodec", videoAudioCodecs.join(','))
});
profile["CodecProfiles"] = codecProfiles;
profile["ContainerProfiles"] = QJsonArray();
profile["DirectPlayProfiles"] = directPlayProfiles;
profile["ResponseProfiles"] = responseProfiles;
profile["SubtitleProfiles"] = QJsonArray();
profile["TranscodingProfiles"] = transcodingProfiles;
profile["MaxStreamingBitrate"] = maxStreamingBitrate();
return profile;
}
}

263
core/src/jellyfinitem.cpp Normal file
View file

@ -0,0 +1,263 @@
#include "jellyfinitem.h"
namespace Jellyfin {
const QRegularExpression JsonSerializable::m_listExpression = QRegularExpression("^QList<\\s*([a-zA-Z0-9]*)\\s*\\*?\\s*>$");
JsonSerializable::JsonSerializable(QObject *parent) : QObject(parent) {
}
void JsonSerializable::deserialize(const QJsonObject &jObj) {
const QMetaObject *obj = this->metaObject();
// Loop over each property,
for (int i = 0; i < obj->propertyCount(); i++) {
QMetaProperty prop = obj->property(i);
// Skip properties which are not stored (usually derrived of other properties)
if (!prop.isStored()) continue;
if (!prop.isWritable()) continue;
qDebug() << toPascalCase(prop.name());
// Hardcoded exception for the property id, since its special inside QML
if (QString(prop.name()) == "jellyfinId" && jObj.contains("Id")) {
QJsonValue val = jObj["Id"];
prop.write(this, jsonToVariant(prop, val, jObj));
} else if (jObj.contains(toPascalCase(prop.name()))) {
QJsonValue val = jObj[toPascalCase(prop.name())];
prop.write(this, jsonToVariant(prop, val, jObj));
} else if (QString(prop.name()).startsWith("__list__")) {
// QML doesn't like it if we expose properties of type QList<SubclassOfQobject *>
// we need to explicitly cast them to QList<QObject *> or QQmlListProperty, which sucks.
// That's why we have a special case for properties starting with __list__. This contains
// the actual QList<SubclassOfQobject *>, so that qml can access the object with its real name.
QString realName = toPascalCase(prop.name() + 8);
if (!jObj.contains(realName)) {
qDebug() << "Ignoring " << realName << " - " << prop.name();
continue;
}
QJsonValue val = jObj[realName];
qDebug() << realName << " - " << prop.name() << ": " << val;
QMetaProperty realProp = obj->property(obj->indexOfProperty(prop.name() + 8));
if (!realProp.write(this, jsonToVariant(prop, val, jObj))) {
qDebug() << "Write to " << prop.name() << "failed";
};
} else {
qDebug() << "Ignored " << prop.name() << " while deserializing";
}
}
}
QVariant JsonSerializable::jsonToVariant(QMetaProperty prop, const QJsonValue &val, const QJsonObject &root) {
switch(val.type()) {
case QJsonValue::Null:
case QJsonValue::Undefined:
return QVariant();
case QJsonValue::Bool:
case QJsonValue::Double:
case QJsonValue::String:
return val.toVariant();
case QJsonValue::Array:
{
QJsonArray arr = val.toArray();
QVariantList varArr;
for (auto it = arr.constBegin(); it < arr.constEnd(); it++) {
QVariant variant = jsonToVariant(prop, *it, root);
qDebug() << variant;
varArr.append(variant);
}
qDebug() << prop.name() << ": " << varArr.count();
return QVariant(varArr);
}
case QJsonValue::Object:
QJsonObject innerObj = val.toObject();
int typeNo = prop.userType();
const QMetaObject *metaType = QMetaType::metaObjectForType(prop.userType());
if (metaType == nullptr) {
// Try to determine if the type is a qlist
QRegularExpressionMatch match = m_listExpression.match(prop.typeName());
if (match.hasMatch()) {
// It is a qList! Now extract the inner type
// There should be an easier way, shouldn't there?
QString listType = match.captured(1).prepend("Jellyfin::").append("*");
// UGLY CODE HERE WE COME
typeNo = QMetaType::type(listType.toUtf8());
if (typeNo == QMetaType::UnknownType) {
qDebug() << "Unknown type: " << listType;
return QVariant();
}
metaType = QMetaType::metaObjectForType(typeNo);
} else {
qDebug() << "No metaObject for " << prop.typeName() << ", " << prop.type() << ", " << prop.userType();
return QVariant();
}
}
QObject *deserializedInnerObj = metaType->newInstance();
deserializedInnerObj->setParent(this);
if (JsonSerializable *ser = dynamic_cast<JsonSerializable *>(deserializedInnerObj)) {
qDebug() << "Deserializing user type " << deserializedInnerObj->metaObject()->className();
ser->deserialize(innerObj);
return QVariant(typeNo, &ser);
} else {
deserializedInnerObj->deleteLater();
qDebug() << "Object is not a serializable one!";
return QVariant();
}
}
return QVariant();
}
QJsonObject JsonSerializable::serialize() const {
QJsonObject result;
const QMetaObject *obj = this->metaObject();
for (int i = 0; i < obj->propertyCount(); i++) {
QMetaProperty prop = obj->property(i);
if (QString(prop.name()) == "jellyfinId") {
result["Id"] = variantToJson(prop.read(this));
} else {
result[toPascalCase(prop.name())] = variantToJson(prop.read(this));
}
}
return result;
}
QJsonValue JsonSerializable::variantToJson(const QVariant var) const {
switch(var.type()) {
case QVariant::Invalid:
return QJsonValue();
case QVariant::UserType:
if (var.canConvert<JsonSerializable *>()) {
JsonSerializable * obj = var.value<JsonSerializable *>();
return obj->serialize();
} else {
qWarning() << "Not serializable: " << var.typeName();
return QJsonValue();
}
case QVariant::Bool:
return var.toBool();
case QVariant::List:
{
QVariantList list = var.toList();
QJsonArray arr;
for (auto it = list.begin(); it < list.end(); it++) {
arr << variantToJson(*it);
}
return arr;
}
default:
if (var.canConvert(QVariant::Double)) {
return var.toDouble();
} if (var.canConvert(QVariant::String)) {
return var.toString();
} else {
return QJsonValue();
}
}
}
QString JsonSerializable::toPascalCase(QString str) {
str[0] = str[0].toUpper();
return str;
}
QString JsonSerializable::fromPascalCase(QString str) {
str[0] = str[0].toLower();
return str;
}
// RemoteData
RemoteData::RemoteData(QObject *parent) : JsonSerializable (parent) {}
void RemoteData::setStatus(Status newStatus) {
m_status = newStatus;
emit statusChanged(newStatus);
}
void RemoteData::setError(QNetworkReply::NetworkError error) {
m_error = error;
emit errorChanged(error);
}
void RemoteData::setErrorString(const QString &newErrorString) {
m_errorString = newErrorString;
emit errorStringChanged(newErrorString);
}
void RemoteData::setApiClient(ApiClient *newApiClient) {
m_apiClient = newApiClient;
emit apiClientChanged(newApiClient);
reload();
}
// MediaStream
MediaStream::MediaStream(QObject *parent) : JsonSerializable (parent) {}
MediaStream::MediaStream(const MediaStream &other)
: JsonSerializable (other.parent()),
m_codec(other.m_codec),
m_codecTag(other.m_codecTag),
m_language(other.m_language),
m_displayTitle(other.m_displayTitle),
m_type(other.m_type),
m_index(other.m_index){
}
bool MediaStream::operator==(const MediaStream &other) {
// displayTitle is explicitly left out, since it's generated based on other properties
// in the Jellyfin source code.
return m_codec == other.m_codec && m_codecTag == other.m_codecTag
&& m_language == other.m_language && m_type == other.m_type
&& m_index == other.m_index;
}
// Item
Item::Item(QObject *parent) : RemoteData(parent) {}
void Item::setJellyfinId(QString newId) {
m_id = newId.trimmed();
if (m_id != newId) {
emit jellyfinIdChanged(m_id);
reload();
}
}
void Item::reload() {
if (m_id.isEmpty() || m_apiClient == nullptr) {
setStatus(Uninitialised);
return;
} else {
setStatus(Loading);
}
QNetworkReply *rep = m_apiClient->get("/Users/" + m_apiClient->userId() + "/Items/" + m_id);
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
rep->deleteLater();
QJsonParseError error;
QString data(rep->readAll());
data = data.normalized(QString::NormalizationForm_D);
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &error);
if (doc.isNull()) {
this->setError(QNetworkReply::ProtocolFailure);
this->setErrorString(error.errorString());
return;
}
if (!doc.isObject()) {
this->setError(QNetworkReply::ProtocolFailure);
this->setErrorString(tr("Invalid response from the server: root element is not an object."));
return;
}
this->deserialize(doc.object());
this->setStatus(Ready);
});
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, [this, rep](QNetworkReply::NetworkError error) {
rep->deleteLater();
this->setError(error);
this->setErrorString(rep->errorString());
this->setStatus(Error);
});
}
void registerSerializableJsonTypes(const char* URI) {
qmlRegisterType<MediaStream>(URI, 1, 0, "MediaStream");
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
}
}

View file

@ -0,0 +1,163 @@
/*
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 "jellyfinplaybackmanager.h"
namespace Jellyfin {
PlaybackManager::PlaybackManager(QObject *parent)
: QObject(parent) {
m_updateTimer.setInterval(10000); // 10 seconds
m_updateTimer.setSingleShot(false);
connect(&m_updateTimer, &QTimer::timeout, this, &PlaybackManager::updatePlaybackInfo);
}
void PlaybackManager::fetchStreamUrl() {
QUrlQuery params;
params.addQueryItem("UserId", m_apiClient->userId());
params.addQueryItem("StartTimeTicks", QString::number(m_position));
params.addQueryItem("IsPlayback", "true");
params.addQueryItem("AutoOpenLiveStream", this->m_autoOpen ? "true" : "false");
params.addQueryItem("MediaSourceId", this->m_itemId);
params.addQueryItem("SubtitleStreamIndex", QString::number(m_subtitleIndex));
params.addQueryItem("AudioStreamIndex", QString::number(m_audioIndex));
QJsonObject root;
root["DeviceProfile"] = m_apiClient->playbackDeviceProfile();
QNetworkReply *rep = m_apiClient->post("/Items/" + this->m_itemId + "/PlaybackInfo", QJsonDocument(root), params);
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
QJsonObject root = QJsonDocument::fromJson(rep->readAll()).object();
this->m_playSessionId = root["PlaySessionId"].toString();
qDebug() << "Session id: " << this->m_playSessionId;
if (this->m_autoOpen) {
QJsonArray mediaSources = root["MediaSources"].toArray();
//FIXME: relies on the fact that the returned transcode url always has a query!
this->m_streamUrl = this->m_apiClient->baseUrl()
+ mediaSources[0].toObject()["TranscodingUrl"].toString();
this->m_playMethod = Transcode;
emit this->streamUrlChanged(this->m_streamUrl);
qDebug() << "Found stream url: " << this->m_streamUrl;
}
rep->deleteLater();
});
}
void PlaybackManager::setItemId(const QString &newItemId) {
if (m_apiClient == nullptr) {
qWarning() << "apiClient is not set on this MediaSource instance! Aborting.";
return;
}
this->m_itemId = newItemId;
// Deinitialize the streamUrl
setStreamUrl("");
if (!newItemId.isEmpty()) {
fetchStreamUrl();
}
}
void PlaybackManager::setStreamUrl(const QString &streamUrl) {
this->m_streamUrl = streamUrl;
emit streamUrlChanged(streamUrl);
}
void PlaybackManager::setPosition(qint64 position) {
if (position == 0 && m_position != 0) {
// Save the old position when stop gets called. The QMediaPlayer will try to set
// position to 0 when stopped, but we don't want to report that to Jellyfin. We
// want the old position.
m_stopPosition = m_position;
}
m_position = position;
emit positionChanged(position);
}
void PlaybackManager::setState(QMediaPlayer::State newState) {
if (m_state == newState) return;
if (m_state == QMediaPlayer::StoppedState) {
// We're transitioning from stopped to either playing or paused.
// Set up the recurring timer
m_updateTimer.start();
postPlaybackInfo(Started);
} else if (newState == QMediaPlayer::StoppedState) {
// We've stopped playing the media. Post a stop signal.
m_updateTimer.stop();
postPlaybackInfo(Stopped);
} else {
postPlaybackInfo(Progress);
}
m_state = newState;
emit this->stateChanged(newState);
}
void PlaybackManager::updatePlaybackInfo() {
postPlaybackInfo(Progress);
}
void PlaybackManager::postPlaybackInfo(PlaybackInfoType type) {
QJsonObject root;
root["ItemId"] = m_itemId;
root["SessionId"] = m_playSessionId;
switch(type) {
case Started: // FALLTHROUGH
case Progress:
root["IsPaused"] = m_state != QMediaPlayer::PlayingState;
root["IsMuted"] = false;
root["AudioStreamIndex"] = m_audioIndex;
root["SubtitleStreamIndex"] = m_subtitleIndex;
root["PlayMethod"] = QVariant::fromValue(m_playMethod).toString();
root["PositionTicks"] = m_position;
break;
case Stopped:
root["PositionTicks"] = m_stopPosition;
break;
}
QString path;
switch (type) {
case Started:
path = "/Sessions/Playing";
break;
case Progress:
path = "/Sessions/Playing/Progress";
break;
case Stopped:
path = "/Sessions/Playing/Stopped";
break;
}
QNetworkReply *rep = m_apiClient->post(path, QJsonDocument(root));
connect(rep, &QNetworkReply::finished, this, [rep](){
rep->deleteLater();
});
m_apiClient->setDefaultErrorHandler(rep);
}
}

View file

@ -0,0 +1,114 @@
/*
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, &QWebSocket::disconnected, this, &WebSocket::onDisconnected);
connect(&m_webSocket, static_cast<void (QWebSocket::*)(QAbstractSocket::SocketError)>(&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::onDisconnected() {
disconnect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocket::textMessageReceived);
m_keepAliveTimer.stop();
}
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<MessageType>(QMetaEnum::fromType<WebSocket::MessageType>().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;
}
}

View file

@ -0,0 +1,93 @@
/*
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 "serverdiscoverymodel.h"
namespace Jellyfin {
ServerDiscoveryModel::ServerDiscoveryModel(QObject *parent)
: QAbstractListModel (parent) {
connect(&m_socket, &QUdpSocket::readyRead, this, &ServerDiscoveryModel::on_datagramsAvailable);
m_socket.bind(BROADCAST_PORT);
}
QVariant ServerDiscoveryModel::data(const QModelIndex &index, int role) const {
if (index.row() < 0 || index.row() >= rowCount()) return QVariant();
size_t row = static_cast<size_t>(index.row());
switch(role) {
case ROLE_ADDRESS:
return m_discoveredServers[row].address;
case ROLE_ID:
return m_discoveredServers[row].id;
case ROLE_NAME:
return m_discoveredServers[row].name;
default:
return QVariant();
}
}
void ServerDiscoveryModel::refresh() {
this->beginResetModel();
this->m_discoveredServers.clear();
this->endResetModel();
m_socket.writeDatagram(MAGIC_PACKET, QHostAddress::Broadcast, BROADCAST_PORT);
}
void ServerDiscoveryModel::on_datagramsAvailable() {
int beginIndex = static_cast<int>(m_discoveredServers.size());
QByteArray datagram;
QJsonDocument jsonDocument;
QJsonParseError jsonParseError;
QHostAddress replyAddress;
std::vector<ServerDiscovery> discoveredServers;
while (m_socket.hasPendingDatagrams()) {
datagram.resize(static_cast<int>(m_socket.pendingDatagramSize()));
m_socket.readDatagram(datagram.data(), datagram.size(), &replyAddress);
jsonDocument = QJsonDocument::fromJson(datagram, &jsonParseError);
// Check if parsing failed
if (jsonDocument.isNull()) {
qDebug() << "Invalid response from " << replyAddress.toString() << ": " << jsonParseError.errorString();
continue;
}
if (jsonDocument.isObject()) {
QJsonObject rootObject = jsonDocument.object();
if (rootObject.contains("Name") && rootObject.contains("Address") && rootObject.contains("Id")) {
// We (assume) we have a correct response! Add it to the back of our temporary vector with discovered servers
discoveredServers.push_back(ServerDiscovery {
rootObject["Name"].toString(),
rootObject["Address"].toString(),
rootObject["Id"].toString()
});
} else {
qDebug() << "Invalid response from " << replyAddress.toString() << ": does not contain Name, Address, or Id field";
}
} else {
qDebug() << "Invalid response from " << replyAddress.toString() << ": root is not an object";
}
}
beginInsertRows(QModelIndex(), beginIndex, beginIndex + static_cast<int>(discoveredServers.size()) - 1);
m_discoveredServers.insert(m_discoveredServers.end(), discoveredServers.begin(), discoveredServers.end());
endInsertRows();
};
}