Refactor Tidal, Spotify, Qobuz, Subsonic and cover providers

Use common HTTP, Json and OAuthenticator class
This commit is contained in:
Jonas Kvinge
2025-03-08 23:11:07 +01:00
parent 7de8a44709
commit cd516c37b9
81 changed files with 2429 additions and 3968 deletions

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,10 +19,8 @@
#include "config.h"
#include <utility>
#include <memory>
#include <QObject>
#include <QByteArray>
#include <QPair>
#include <QList>
@@ -37,6 +35,7 @@
#include <QJsonObject>
#include <QSettings>
#include <QSslError>
#include <QScopedPointer>
#include "includes/shared_ptr.h"
#include "core/logging.h"
@@ -173,7 +172,7 @@ QobuzService::~QobuzService() {
}
while (!stream_url_requests_.isEmpty()) {
SharedPtr<QobuzStreamURLRequest> stream_url_req = stream_url_requests_.take(stream_url_requests_.firstKey());
QSharedPointer<QobuzStreamURLRequest> stream_url_req = stream_url_requests_.take(stream_url_requests_.firstKey());
QObject::disconnect(&*stream_url_req, nullptr, this, nullptr);
}
@@ -218,7 +217,7 @@ void QobuzService::ReloadSettings() {
const bool base64_secret = s.value(QobuzSettings::kBase64Secret, false).toBool();;
username_ = s.value(QobuzSettings::kUsername).toString();
QByteArray password = s.value(QobuzSettings::kPassword).toByteArray();
const QByteArray password = s.value(QobuzSettings::kPassword).toByteArray();
if (password.isEmpty()) password_.clear();
else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
@@ -267,7 +266,6 @@ void QobuzService::SendLogin() {
void QobuzService::SendLoginWithCredentials(const QString &app_id, const QString &username, const QString &password) {
Q_EMIT UpdateStatus(tr("Authenticating..."));
login_errors_.clear();
login_sent_ = true;
++login_attempts_;
@@ -285,14 +283,13 @@ void QobuzService::SendLoginWithCredentials(const QString &app_id, const QString
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(kAuthUrl));
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
const QUrl url(QString::fromLatin1(kAuthUrl));
QNetworkRequest network_request(url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
const QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(network_request, query);
replies_ << reply;
QObject::connect(reply, &QNetworkReply::sslErrors, this, &QobuzService::HandleLoginSSLErrors);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { HandleAuthReply(reply); });
@@ -304,7 +301,7 @@ void QobuzService::SendLoginWithCredentials(const QString &app_id, const QString
void QobuzService::HandleLoginSSLErrors(const QList<QSslError> &ssl_errors) {
for (const QSslError &ssl_error : ssl_errors) {
login_errors_ += ssl_error.errorString();
qLog(Debug) << "Qobuz" << ssl_error.errorString();
}
}
@@ -321,115 +318,111 @@ void QobuzService::HandleAuthReply(QNetworkReply *reply) {
LoginError(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
return;
}
else {
// See if there is Json data containing "status", "code" and "message" - then use that instead.
QByteArray data(reply->readAll());
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (!json_obj.isEmpty() && json_obj.contains("status"_L1) && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) {
int code = json_obj["code"_L1].toInt();
QString message = json_obj["message"_L1].toString();
login_errors_ << QStringLiteral("%1 (%2)").arg(message).arg(code);
}
// See if there is Json data containing "status", "code" and "message" - then use that instead.
const QByteArray data = reply->readAll();
QString error_message;
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_document.isEmpty() && json_document.isObject()) {
const QJsonObject json_object = json_document.object();
if (!json_object.isEmpty() && json_object.contains("status"_L1) && json_object.contains("code"_L1) && json_object.contains("message"_L1)) {
const int code = json_object["code"_L1].toInt();
const QString message = json_object["message"_L1].toString();
error_message = QStringLiteral("%1 (%2)").arg(message).arg(code);
}
if (login_errors_.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
login_errors_ << QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
login_errors_ << QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
LoginError();
return;
}
if (error_message.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error_message = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
LoginError(error_message);
return;
}
login_errors_.clear();
const QByteArray data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
LoginError(u"Authentication reply from server missing Json data."_s);
return;
}
if (json_doc.isEmpty()) {
if (json_document.isEmpty()) {
LoginError(u"Authentication reply from server has empty Json document."_s);
return;
}
if (!json_doc.isObject()) {
LoginError(u"Authentication reply from server has Json document that is not an object."_s, json_doc);
if (!json_document.isObject()) {
LoginError(u"Authentication reply from server has Json document that is not an object."_s, json_document);
return;
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
LoginError(u"Authentication reply from server has empty Json object."_s, json_doc);
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
LoginError(u"Authentication reply from server has empty Json object."_s, json_document);
return;
}
if (!json_obj.contains("user_auth_token"_L1)) {
LoginError(u"Authentication reply from server is missing user_auth_token"_s, json_obj);
if (!json_object.contains("user_auth_token"_L1)) {
LoginError(u"Authentication reply from server is missing user_auth_token"_s, json_object);
return;
}
user_auth_token_ = json_obj["user_auth_token"_L1].toString();
user_auth_token_ = json_object["user_auth_token"_L1].toString();
if (!json_obj.contains("user"_L1)) {
LoginError(u"Authentication reply from server is missing user"_s, json_obj);
if (!json_object.contains("user"_L1)) {
LoginError(u"Authentication reply from server is missing user"_s, json_object);
return;
}
QJsonValue value_user = json_obj["user"_L1];
const QJsonValue value_user = json_object["user"_L1];
if (!value_user.isObject()) {
LoginError(u"Authentication reply user is not a object"_s, json_obj);
LoginError(u"Authentication reply user is not a object"_s, json_object);
return;
}
QJsonObject obj_user = value_user.toObject();
const QJsonObject object_user = value_user.toObject();
if (!obj_user.contains("id"_L1)) {
LoginError(u"Authentication reply from server is missing user id"_s, obj_user);
if (!object_user.contains("id"_L1)) {
LoginError(u"Authentication reply from server is missing user id"_s, object_user);
return;
}
user_id_ = obj_user["id"_L1].toInt();
user_id_ = object_user["id"_L1].toInt();
if (!obj_user.contains("device"_L1)) {
LoginError(u"Authentication reply from server is missing user device"_s, obj_user);
if (!object_user.contains("device"_L1)) {
LoginError(u"Authentication reply from server is missing user device"_s, object_user);
return;
}
QJsonValue value_device = obj_user["device"_L1];
const QJsonValue value_device = object_user["device"_L1];
if (!value_device.isObject()) {
LoginError(u"Authentication reply from server user device is not a object"_s, value_device);
return;
}
QJsonObject obj_device = value_device.toObject();
const QJsonObject object_device = value_device.toObject();
if (!obj_device.contains("device_manufacturer_id"_L1)) {
LoginError(u"Authentication reply from server device is missing device_manufacturer_id"_s, obj_device);
if (!object_device.contains("device_manufacturer_id"_L1)) {
LoginError(u"Authentication reply from server device is missing device_manufacturer_id"_s, object_device);
return;
}
device_id_ = obj_device["device_manufacturer_id"_L1].toString();
device_id_ = object_device["device_manufacturer_id"_L1].toString();
if (!obj_user.contains("credential"_L1)) {
LoginError(u"Authentication reply from server is missing user credential"_s, obj_user);
if (!object_user.contains("credential"_L1)) {
LoginError(u"Authentication reply from server is missing user credential"_s, object_user);
return;
}
QJsonValue value_credential = obj_user["credential"_L1];
const QJsonValue value_credential = object_user["credential"_L1];
if (!value_credential.isObject()) {
LoginError(u"Authentication reply from serve userr credential is not a object"_s, value_device);
return;
}
QJsonObject obj_credential = value_credential.toObject();
const QJsonObject object_credential = value_credential.toObject();
if (!obj_credential.contains("id"_L1)) {
LoginError(u"Authentication reply user credential from server is missing user credential id"_s, obj_credential);
if (!object_credential.contains("id"_L1)) {
LoginError(u"Authentication reply user credential from server is missing user credential id"_s, object_credential);
return;
}
credential_id_ = obj_credential["id"_L1].toInt();
credential_id_ = object_credential["id"_L1].toInt();
Settings s;
s.beginGroup(QobuzSettings::kSettingsGroup);
@@ -444,12 +437,12 @@ void QobuzService::HandleAuthReply(QNetworkReply *reply) {
login_attempts_ = 0;
if (timer_login_attempt_->isActive()) timer_login_attempt_->stop();
Q_EMIT LoginComplete(true);
Q_EMIT LoginFinished(true);
Q_EMIT LoginSuccess();
}
void QobuzService::Logout() {
void QobuzService::ClearSession() {
user_auth_token_.clear();
device_id_.clear();
@@ -475,19 +468,19 @@ void QobuzService::TryLogin() {
if (authenticated() || login_sent_) return;
if (login_attempts_ >= kLoginAttempts) {
Q_EMIT LoginComplete(false, tr("Maximum number of login attempts reached."));
Q_EMIT LoginFinished(false, tr("Maximum number of login attempts reached."));
return;
}
if (app_id_.isEmpty()) {
Q_EMIT LoginComplete(false, tr("Missing Qobuz app ID."));
Q_EMIT LoginFinished(false, tr("Missing Qobuz app ID."));
return;
}
if (username_.isEmpty()) {
Q_EMIT LoginComplete(false, tr("Missing Qobuz username."));
Q_EMIT LoginFinished(false, tr("Missing Qobuz username."));
return;
}
if (password_.isEmpty()) {
Q_EMIT LoginComplete(false, tr("Missing Qobuz password."));
Q_EMIT LoginFinished(false, tr("Missing Qobuz password."));
return;
}
@@ -517,8 +510,7 @@ void QobuzService::GetArtists() {
return;
}
ResetArtistsRequest();
artists_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteArtists), [](QobuzRequest *request) { request->deleteLater(); });
artists_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteArtists));
QObject::connect(&*artists_request_, &QobuzRequest::Results, this, &QobuzService::ArtistsResultsReceived);
QObject::connect(&*artists_request_, &QobuzRequest::UpdateStatus, this, &QobuzService::ArtistsUpdateStatusReceived);
QObject::connect(&*artists_request_, &QobuzRequest::UpdateProgress, this, &QobuzService::ArtistsUpdateProgressReceived);
@@ -567,8 +559,7 @@ void QobuzService::GetAlbums() {
return;
}
ResetAlbumsRequest();
albums_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteAlbums), [](QobuzRequest *request) { request->deleteLater(); });
albums_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteAlbums));
QObject::connect(&*albums_request_, &QobuzRequest::Results, this, &QobuzService::AlbumsResultsReceived);
QObject::connect(&*albums_request_, &QobuzRequest::UpdateStatus, this, &QobuzService::AlbumsUpdateStatusReceived);
QObject::connect(&*albums_request_, &QobuzRequest::UpdateProgress, this, &QobuzService::AlbumsUpdateProgressReceived);
@@ -617,8 +608,7 @@ void QobuzService::GetSongs() {
return;
}
ResetSongsRequest();
songs_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteSongs), [](QobuzRequest *request) { request->deleteLater(); });
songs_request_.reset(new QobuzRequest(this, url_handler_, network_, QobuzBaseRequest::Type::FavouriteSongs));
QObject::connect(&*songs_request_, &QobuzRequest::Results, this, &QobuzService::SongsResultsReceived);
QObject::connect(&*songs_request_, &QobuzRequest::UpdateStatus, this, &QobuzService::SongsUpdateStatusReceived);
QObject::connect(&*songs_request_, &QobuzRequest::UpdateProgress, this, &QobuzService::SongsUpdateProgressReceived);
@@ -678,8 +668,7 @@ void QobuzService::StartSearch() {
}
void QobuzService::CancelSearch() {
}
void QobuzService::CancelSearch() {}
void QobuzService::SendSearch() {
@@ -697,12 +686,10 @@ void QobuzService::SendSearch() {
break;
}
search_request_.reset(new QobuzRequest(this, url_handler_, network_, query_type), [](QobuzRequest *request) { request->deleteLater(); } );
search_request_.reset(new QobuzRequest(this, url_handler_, network_, query_type));
QObject::connect(&*search_request_, &QobuzRequest::Results, this, &QobuzService::SearchResultsReceived);
QObject::connect(&*search_request_, &QobuzRequest::UpdateStatus, this, &QobuzService::SearchUpdateStatus);
QObject::connect(&*search_request_, &QobuzRequest::UpdateProgress, this, &QobuzService::SearchUpdateProgress);
search_request_->Search(search_id_, search_text_);
search_request_->Process();
@@ -724,16 +711,15 @@ uint QobuzService::GetStreamURL(const QUrl &url, QString &error) {
uint id = 0;
while (id == 0) id = ++next_stream_url_request_id_;
SharedPtr<QobuzStreamURLRequest> stream_url_req;
stream_url_req.reset(new QobuzStreamURLRequest(this, network_, url, id), [](QobuzStreamURLRequest *request) { request->deleteLater(); });
stream_url_requests_.insert(id, stream_url_req);
QobuzStreamURLRequestPtr stream_url_request = QobuzStreamURLRequestPtr(new QobuzStreamURLRequest(this, network_, url, id), &QObject::deleteLater);
stream_url_requests_.insert(id, stream_url_request);
QObject::connect(&*stream_url_req, &QobuzStreamURLRequest::TryLogin, this, &QobuzService::TryLogin);
QObject::connect(&*stream_url_req, &QobuzStreamURLRequest::StreamURLFailure, this, &QobuzService::HandleStreamURLFailure);
QObject::connect(&*stream_url_req, &QobuzStreamURLRequest::StreamURLSuccess, this, &QobuzService::HandleStreamURLSuccess);
QObject::connect(this, &QobuzService::LoginComplete, &*stream_url_req, &QobuzStreamURLRequest::LoginComplete);
QObject::connect(&*stream_url_request, &QobuzStreamURLRequest::TryLogin, this, &QobuzService::TryLogin);
QObject::connect(&*stream_url_request, &QobuzStreamURLRequest::StreamURLFailure, this, &QobuzService::HandleStreamURLFailure);
QObject::connect(&*stream_url_request, &QobuzStreamURLRequest::StreamURLSuccess, this, &QobuzService::HandleStreamURLSuccess);
QObject::connect(this, &QobuzService::LoginFinished, &*stream_url_request, &QobuzStreamURLRequest::LoginComplete);
stream_url_req->Process();
stream_url_request->Process();
return id;
@@ -759,18 +745,10 @@ void QobuzService::HandleStreamURLSuccess(const uint id, const QUrl &media_url,
void QobuzService::LoginError(const QString &error, const QVariant &debug) {
if (!error.isEmpty()) login_errors_ << error;
QString error_html;
for (const QString &e : std::as_const(login_errors_)) {
qLog(Error) << "Qobuz:" << e;
error_html += e + u"<br />"_s;
}
qLog(Error) << "Qobuz:" << error;
if (debug.isValid()) qLog(Debug) << debug;
Q_EMIT LoginFailure(error_html);
Q_EMIT LoginComplete(false, error_html);
login_errors_.clear();
Q_EMIT LoginFailure(error);
Q_EMIT LoginFinished(false, error);
}