From cd516c37b94d4d850d5fa593b7eecf72a056322e Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 8 Mar 2025 23:11:07 +0100 Subject: [PATCH] Refactor Tidal, Spotify, Qobuz, Subsonic and cover providers Use common HTTP, Json and OAuthenticator class --- CMakeLists.txt | 3 +- src/covermanager/albumcoverfetchersearch.cpp | 2 +- src/covermanager/coverprovider.cpp | 5 +- src/covermanager/coverprovider.h | 26 +- src/covermanager/deezercoverprovider.cpp | 228 ++++----- src/covermanager/deezercoverprovider.h | 16 +- src/covermanager/discogscoverprovider.cpp | 202 ++++---- src/covermanager/discogscoverprovider.h | 12 +- src/covermanager/jsoncoverprovider.cpp | 25 +- src/covermanager/jsoncoverprovider.h | 5 +- src/covermanager/lastfmcoverprovider.cpp | 207 ++++---- src/covermanager/lastfmcoverprovider.h | 16 +- src/covermanager/musicbrainzcoverprovider.cpp | 180 +++---- src/covermanager/musicbrainzcoverprovider.h | 10 +- src/covermanager/musixmatchcoverprovider.cpp | 128 ++--- src/covermanager/musixmatchcoverprovider.h | 12 +- src/covermanager/opentidalcoverprovider.cpp | 380 +++++--------- src/covermanager/opentidalcoverprovider.h | 24 +- src/covermanager/qobuzcoverprovider.cpp | 191 +++---- src/covermanager/qobuzcoverprovider.h | 23 +- src/covermanager/spotifycoverprovider.cpp | 222 ++++----- src/covermanager/spotifycoverprovider.h | 31 +- src/covermanager/tidalcoverprovider.cpp | 198 ++++---- src/covermanager/tidalcoverprovider.h | 27 +- src/lyrics/musixmatchlyricsprovider.cpp | 10 +- src/lyrics/musixmatchlyricsprovider.h | 3 +- src/qobuz/qobuzbaserequest.cpp | 194 ++++---- src/qobuz/qobuzbaserequest.h | 57 +-- src/qobuz/qobuzfavoriterequest.cpp | 38 +- src/qobuz/qobuzfavoriterequest.h | 10 +- src/qobuz/qobuzrequest.cpp | 299 +++++------ src/qobuz/qobuzrequest.h | 31 +- src/qobuz/qobuzservice.cpp | 192 ++++---- src/qobuz/qobuzservice.h | 22 +- src/qobuz/qobuzstreamurlrequest.cpp | 80 ++- src/qobuz/qobuzstreamurlrequest.h | 12 +- src/qobuz/qobuzurlhandler.cpp | 3 +- src/qobuz/qobuzurlhandler.h | 4 +- src/settings/coverssettingspage.cpp | 28 +- src/settings/coverssettingspage.h | 8 +- src/settings/qobuzsettingspage.cpp | 11 +- src/settings/qobuzsettingspage.h | 6 +- src/settings/spotifysettingspage.cpp | 11 +- src/settings/spotifysettingspage.h | 6 +- src/settings/tidalsettingspage.cpp | 11 +- src/settings/tidalsettingspage.h | 6 +- src/spotify/spotifybaserequest.cpp | 215 +++----- src/spotify/spotifybaserequest.h | 49 +- src/spotify/spotifyfavoriterequest.cpp | 76 ++- src/spotify/spotifyfavoriterequest.h | 9 +- src/spotify/spotifyrequest.cpp | 328 ++++++------ src/spotify/spotifyrequest.h | 26 +- src/spotify/spotifyservice.cpp | 421 +++------------- src/spotify/spotifyservice.h | 66 +-- src/streaming/streamingservice.h | 11 +- src/streaming/streamingsongsview.cpp | 24 +- src/streaming/streamingsongsview.h | 1 + src/subsonic/subsonicbaserequest.cpp | 155 +++--- src/subsonic/subsonicbaserequest.h | 12 +- src/subsonic/subsonicrequest.cpp | 284 +++++------ src/subsonic/subsonicrequest.h | 4 +- src/subsonic/subsonicscrobblerequest.cpp | 34 +- src/subsonic/subsonicscrobblerequest.h | 2 +- src/subsonic/subsonicservice.cpp | 14 +- src/subsonic/subsonicservice.h | 8 +- src/subsonic/subsonicurlhandler.cpp | 3 +- src/subsonic/subsonicurlhandler.h | 5 +- src/tidal/tidalbaserequest.cpp | 220 +++------ src/tidal/tidalbaserequest.h | 46 +- src/tidal/tidalfavoriterequest.cpp | 59 +-- src/tidal/tidalfavoriterequest.h | 4 +- src/tidal/tidalrequest.cpp | 351 ++++++------- src/tidal/tidalrequest.h | 20 +- src/tidal/tidalservice.cpp | 466 ++++-------------- src/tidal/tidalservice.h | 70 +-- src/tidal/tidalstreamurlrequest.cpp | 130 ++--- src/tidal/tidalstreamurlrequest.h | 23 +- src/tidal/tidalurlhandler.cpp | 21 +- src/tidal/tidalurlhandler.h | 8 +- .../musixmatchprovider.cpp | 7 +- .../musixmatchprovider.h | 10 +- 81 files changed, 2429 insertions(+), 3968 deletions(-) rename src/{providers => utilities}/musixmatchprovider.cpp (86%) rename src/{providers => utilities}/musixmatchprovider.h (84%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 89821e22b..1dfe2fb36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -486,6 +486,7 @@ set(SOURCES src/utilities/screenutils.cpp src/utilities/textencodingutils.cpp src/utilities/coveroptions.cpp + src/utilities/musixmatchprovider.cpp src/tagreader/tagreaderclient.cpp src/tagreader/tagreaderresult.cpp @@ -697,8 +698,6 @@ set(SOURCES src/lyrics/letraslyricsprovider.cpp src/lyrics/lyricfindlyricsprovider.cpp - src/providers/musixmatchprovider.cpp - src/settings/settingsdialog.cpp src/settings/settingspage.cpp src/settings/settingsitemdelegate.cpp diff --git a/src/covermanager/albumcoverfetchersearch.cpp b/src/covermanager/albumcoverfetchersearch.cpp index fc0868d42..d1f60db75 100644 --- a/src/covermanager/albumcoverfetchersearch.cpp +++ b/src/covermanager/albumcoverfetchersearch.cpp @@ -101,7 +101,7 @@ void AlbumCoverFetcherSearch::Start(SharedPtr cover_providers) { if (!provider->is_enabled()) continue; // Skip any provider that requires authentication but is not authenticated. - if (provider->AuthenticationRequired() && !provider->IsAuthenticated()) { + if (provider->authentication_required() && !provider->authenticated()) { continue; } diff --git a/src/covermanager/coverprovider.cpp b/src/covermanager/coverprovider.cpp index e71554c78..1a081034d 100644 --- a/src/covermanager/coverprovider.cpp +++ b/src/covermanager/coverprovider.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,10 +21,9 @@ #include "config.h" -#include #include #include "includes/shared_ptr.h" #include "coverprovider.h" -CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr network, QObject *parent) : QObject(parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), batch_(batch), allow_missing_album_(allow_missing_album) {} +CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr network, QObject *parent) : JsonBaseRequest(network, parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), batch_(batch), allow_missing_album_(allow_missing_album) {} diff --git a/src/covermanager/coverprovider.h b/src/covermanager/coverprovider.h index d0134a0ab..766d25999 100644 --- a/src/covermanager/coverprovider.h +++ b/src/covermanager/coverprovider.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,26 +24,22 @@ #include "config.h" -#include -#include #include #include #include #include "includes/shared_ptr.h" +#include "core/jsonbaserequest.h" #include "albumcoverfetcher.h" class NetworkAccessManager; -// Each implementation of this interface downloads covers from one online service. -// There are no limitations on what this service might be - last.fm, Amazon, Google Images - you name it. -class CoverProvider : public QObject { +class CoverProvider : public JsonBaseRequest { Q_OBJECT public: explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr network, QObject *parent); - // A name (very short description) of this provider, like "last.fm". QString name() const { return name_; } bool is_enabled() const { return enabled_; } int order() const { return order_; } @@ -54,10 +50,14 @@ class CoverProvider : public QObject { void set_enabled(const bool enabled) { enabled_ = enabled; } void set_order(const int order) { order_ = order; } - bool AuthenticationRequired() const { return authentication_required_; } - virtual bool IsAuthenticated() const { return true; } + virtual QString service_name() const override { return name_; } + virtual bool authentication_required() const override { return authentication_required_; } + virtual bool authenticated() const override { return true; } + virtual bool use_authorization_header() const override { return false; } + virtual QByteArray authorization_header() const override { return QByteArray(); } + virtual void Authenticate() {} - virtual void Deauthenticate() {} + virtual void ClearSession() {} // Starts searching for covers matching the given query text. // Returns true if the query has been started, or false if an error occurred. @@ -65,12 +65,10 @@ class CoverProvider : public QObject { virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0; virtual void CancelSearch(const int id) { Q_UNUSED(id); } - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - Q_SIGNALS: - void AuthenticationComplete(const bool success, const QStringList &errors = QStringList()); + void AuthenticationFinished(const bool success, const QString &error = QString()); void AuthenticationSuccess(); - void AuthenticationFailure(const QStringList &errors); + void AuthenticationFailure(const QString &error); void SearchResults(const int id, const CoverProviderSearchResults &results); void SearchFinished(const int id, const CoverProviderSearchResults &results); diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp index 23122113b..d36586915 100644 --- a/src/covermanager/deezercoverprovider.cpp +++ b/src/covermanager/deezercoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +22,6 @@ #include #include -#include -#include #include #include #include @@ -38,6 +36,7 @@ #include #include #include +#include #include "core/networkaccessmanager.h" #include "core/logging.h" @@ -52,22 +51,11 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr char kApiUrl[] = "https://api.deezer.com"; constexpr int kLimit = 10; -} +} // namespace DeezerCoverProvider::DeezerCoverProvider(const SharedPtr network, QObject *parent) : JsonCoverProvider(u"Deezer"_s, true, false, 2.0, true, true, network, parent) {} -DeezerCoverProvider::~DeezerCoverProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false; @@ -91,17 +79,7 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu << Param(u"q"_s, query) << Param(u"limit"_s, QString::number(kLimit)); - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QLatin1String(kApiUrl) + QLatin1Char('/') + resource); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kApiUrl) + QLatin1Char('/') + resource), params); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); }); return true; @@ -110,82 +88,56 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu void DeezerCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); } -QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult DeezerCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - QString error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(error); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("error"_L1) && json_object["error"_L1].isObject()) { + const QJsonObject object_error = json_object["error"_L1].toObject(); + if (object_error.contains("code"_L1) && object_error.contains("type"_L1) && object_error.contains("message"_L1)) { + const int code = object_error["code"_L1].toInt(); + const QString type = object_error["type"_L1].toString(); + const QString message = object_error["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1: %2 (%3)").arg(type, message).arg(code); + } + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "error" object - then use that instead. - data = reply->readAll(); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - QString error; - if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (json_obj.contains("error"_L1)) { - QJsonValue value_error = json_obj["error"_L1]; - if (value_error.isObject()) { - QJsonObject obj_error = value_error.toObject(); - int code = obj_error["code"_L1].toInt(); - QString message = obj_error["message"_L1].toString(); - error = QStringLiteral("%1 (%2)").arg(message).arg(code); - } - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; - -} - -QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonObject(); - - if (json_obj.contains("error"_L1)) { - QJsonValue value_error = json_obj["error"_L1]; - if (!value_error.isObject()) { - Error(u"Error missing object"_s, json_obj); - return QJsonValue(); + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); } - QJsonObject obj_error = value_error.toObject(); - const int code = obj_error["code"_L1].toInt(); - QString message = obj_error["message"_L1].toString(); - Error(QStringLiteral("%1 (%2)").arg(message).arg(code)); - return QJsonValue(); } - if (!json_obj.contains("data"_L1) && !json_obj.contains("DATA"_L1)) { - Error(u"Json reply object is missing data."_s, json_obj); - return QJsonValue(); - } - - QJsonValue value_data; - if (json_obj.contains("data"_L1)) value_data = json_obj["data"_L1]; - else value_data = json_obj["DATA"_L1]; - - return value_data; + return result; } @@ -196,25 +148,37 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); + CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonValue value_data = ExtractData(data); - if (!value_data.isArray()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); + const QJsonObject &json_object = json_object_result.json_object; + if (!json_object.isEmpty()) { + return; + } + + QJsonArray array_data; + if (json_object.contains("data"_L1) && json_object["DATA"_L1].isArray()) { + array_data = json_object["data"_L1].toArray(); + } + else if (json_object.contains("DATA"_L1) && json_object["DATA"_L1].isArray()) { + array_data = json_object["data"_L1].toArray(); + } + else { + Error(u"Json reply object is missing data."_s, json_object); return; } - QJsonArray array_data = value_data.toArray(); if (array_data.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); return; } - QMap results; + QMap cover_results; int i = 0; for (const QJsonValue &json_value : std::as_const(array_data)) { @@ -222,52 +186,52 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) Error(u"Invalid Json reply, data array value is not a object."_s); continue; } - QJsonObject json_obj = json_value.toObject(); - QJsonObject obj_album; - if (json_obj.contains("album"_L1) && json_obj["album"_L1].isObject()) { // Song search, so extract the album. - obj_album = json_obj["album"_L1].toObject(); + const QJsonObject value_object = json_value.toObject(); + QJsonObject object_album; + if (value_object.contains("album"_L1) && value_object["album"_L1].isObject()) { // Song search, so extract the album. + object_album = value_object["album"_L1].toObject(); } else { - obj_album = json_obj; + object_album = value_object; } - if (!json_obj.contains("id"_L1) || !obj_album.contains("id"_L1)) { - Error(u"Invalid Json reply, data array value object is missing ID."_s, json_obj); + if (!value_object.contains("id"_L1) || !object_album.contains("id"_L1)) { + Error(u"Invalid Json reply, data array value object is missing ID."_s, value_object); continue; } - if (!obj_album.contains("type"_L1)) { - Error(u"Invalid Json reply, data array value album object is missing type."_s, obj_album); + if (!object_album.contains("type"_L1)) { + Error(u"Invalid Json reply, data array value album object is missing type."_s, object_album); continue; } - QString type = obj_album["type"_L1].toString(); + const QString type = object_album["type"_L1].toString(); if (type != "album"_L1) { - Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, obj_album); + Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, object_album); continue; } - if (!json_obj.contains("artist"_L1)) { - Error(u"Invalid Json reply, data array value object is missing artist."_s, json_obj); + if (!json_object.contains("artist"_L1)) { + Error(u"Invalid Json reply, data array value object is missing artist."_s, json_object); continue; } - QJsonValue value_artist = json_obj["artist"_L1]; + const QJsonValue value_artist = json_object["artist"_L1]; if (!value_artist.isObject()) { Error(u"Invalid Json reply, data array value artist is not a object."_s, value_artist); continue; } - QJsonObject obj_artist = value_artist.toObject(); + const QJsonObject object_artist = value_artist.toObject(); - if (!obj_artist.contains("name"_L1)) { - Error(u"Invalid Json reply, data array value artist object is missing name."_s, obj_artist); + if (!object_artist.contains("name"_L1)) { + Error(u"Invalid Json reply, data array value artist object is missing name."_s, object_artist); continue; } - QString artist = obj_artist["name"_L1].toString(); + const QString artist = object_artist["name"_L1].toString(); - if (!obj_album.contains("title"_L1)) { - Error(u"Invalid Json reply, data array value album object is missing title."_s, obj_album); + if (!object_album.contains("title"_L1)) { + Error(u"Invalid Json reply, data array value album object is missing title."_s, object_album); continue; } - QString album = obj_album["title"_L1].toString(); + const QString album = object_album["title"_L1].toString(); CoverProviderSearchResult cover_result; cover_result.artist = artist; @@ -277,35 +241,29 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) const QList> cover_sizes = QList>() << qMakePair(u"cover_xl"_s, QSize(1000, 1000)) << qMakePair(u"cover_big"_s, QSize(500, 500)); for (const QPair &cover_size : cover_sizes) { - if (!obj_album.contains(cover_size.first)) continue; - QString cover = obj_album[cover_size.first].toString(); + if (!object_album.contains(cover_size.first)) continue; + QString cover = object_album[cover_size.first].toString(); if (!have_cover) { have_cover = true; ++i; } QUrl url(cover); - if (!results.contains(url)) { + if (!cover_results.contains(url)) { cover_result.image_url = url; cover_result.image_size = cover_size.second; cover_result.number = i; - results.insert(url, cover_result); + cover_results.insert(url, cover_result); } } if (!have_cover) { - Error(u"Invalid Json reply, data array value album object is missing cover."_s, obj_album); + Error(u"Invalid Json reply, data array value album object is missing cover."_s, object_album); } } - if (results.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - } - else { - CoverProviderSearchResults cover_results = results.values(); - std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber); - Q_EMIT SearchFinished(id, cover_results); - } + results = cover_results.values(); + std::stable_sort(results.begin(), results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber); } diff --git a/src/covermanager/deezercoverprovider.h b/src/covermanager/deezercoverprovider.h index 5ed4fd254..7e01edb10 100644 --- a/src/covermanager/deezercoverprovider.h +++ b/src/covermanager/deezercoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,13 +22,8 @@ #include "config.h" -#include -#include #include -#include #include -#include -#include #include "jsoncoverprovider.h" @@ -40,7 +35,6 @@ class DeezerCoverProvider : public JsonCoverProvider { public: explicit DeezerCoverProvider(const SharedPtr network, QObject *parent = nullptr); - ~DeezerCoverProvider() override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; @@ -48,13 +42,11 @@ class DeezerCoverProvider : public JsonCoverProvider { private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id); - private: - QByteArray GetReplyData(QNetworkReply *reply); - QJsonValue ExtractData(const QByteArray &data); - void Error(const QString &error, const QVariant &debug = QVariant()) override; + protected: + JsonObjectResult ParseJsonObject(QNetworkReply *reply); private: - QList replies_; + void Error(const QString &error, const QVariant &debug = QVariant()) override; }; #endif // DEEZERCOVERPROVIDER_H diff --git a/src/covermanager/discogscoverprovider.cpp b/src/covermanager/discogscoverprovider.cpp index ac0da0cdd..80377eef9 100644 --- a/src/covermanager/discogscoverprovider.cpp +++ b/src/covermanager/discogscoverprovider.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, Martin Björklund - * Copyright 2016-2021, Jonas Kvinge + * Copyright 2016-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,8 +24,6 @@ #include #include -#include -#include #include #include #include @@ -40,6 +38,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/logging.h" @@ -146,81 +145,88 @@ void DiscogsCoverProvider::SendSearchRequest(SharedPtr; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + for (const Param ¶m : request_params) { + const EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); query_items << QString::fromLatin1(encoded_param.first) + QLatin1Char('=') + QString::fromLatin1(encoded_param.second); url_query.addQueryItem(QString::fromLatin1(encoded_param.first), QString::fromLatin1(encoded_param.second)); } - url.setQuery(url_query); + + QUrl request_url(url); + request_url.setQuery(url_query); // Sign the request - const QByteArray data_to_sign = QStringLiteral("GET\n%1\n%2\n%3").arg(url.host(), url.path(), query_items.join(u'&')).toUtf8(); + const QByteArray data_to_sign = QStringLiteral("GET\n%1\n%2\n%3").arg(request_url.host(), request_url.path(), query_items.join(u'&')).toUtf8(); const QByteArray signature(Utilities::HmacSha256(QByteArray::fromBase64(kSecretKeyB64), data_to_sign)); // Add the signature to the request url_query.addQueryItem(u"Signature"_s, QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(signature.toBase64())))); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); + QNetworkRequest network_request(request_url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + QNetworkReply *reply = network_->get(network_request); replies_ << reply; - qLog(Debug) << "Discogs: Sending request" << url; + qLog(Debug) << "Discogs: Sending request" << request_url; return reply; } -QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult DiscogsCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - QString error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(error); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("message"_L1)) { + result.error_code = ErrorCode::APIError; + result.error_message = json_object["message"_L1].toString(); + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "message" - then use that instead. - data = reply->readAll(); - QString error; - 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.contains("message"_L1)) { - error = json_obj["message"_L1].toString(); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + return result; } @@ -234,37 +240,34 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) if (!requests_search_.contains(id)) return; SharedPtr search = requests_search_.value(id); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - EndSearch(search); + const QScopeGuard end_search = qScopeGuard([this, search]() { EndSearch(search); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - EndSearch(search); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } QJsonValue value_results; - if (json_obj.contains("results"_L1)) { - value_results = json_obj["results"_L1]; + if (json_object.contains("results"_L1)) { + value_results = json_object["results"_L1]; } - else if (json_obj.contains("message"_L1)) { - QString message = json_obj["message"_L1].toString(); - Error(QStringLiteral("%1").arg(message)); - EndSearch(search); + else if (json_object.contains("message"_L1)) { + Error(json_object["message"_L1].toString()); return; } else { - Error(u"Json object is missing results."_s, json_obj); - EndSearch(search); + Error(u"Json object is missing results."_s, json_object); return; } if (!value_results.isArray()) { Error(u"Missing results array."_s, value_results); - EndSearch(search); return; } @@ -275,19 +278,19 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) Error(u"Invalid Json reply, results value is not a object."_s); continue; } - QJsonObject obj_result = value_result.toObject(); - if (!obj_result.contains("id"_L1) || !obj_result.contains("title"_L1) || !obj_result.contains("resource_url"_L1)) { - Error(QStringLiteral("Invalid Json reply, results value object is missing ID, title or resource_url."), obj_result); + const QJsonObject object_result = value_result.toObject(); + if (!object_result.contains("id"_L1) || !object_result.contains("title"_L1) || !object_result.contains("resource_url"_L1)) { + Error(QStringLiteral("Invalid Json reply, results value object is missing ID, title or resource_url."), object_result); continue; } - quint64 release_id = obj_result["id"_L1].toInt(); - QUrl resource_url(obj_result["resource_url"_L1].toString()); - QString title = obj_result["title"_L1].toString(); + const quint64 release_id = object_result["id"_L1].toInt(); + const QUrl resource_url(object_result["resource_url"_L1].toString()); + QString title = object_result["title"_L1].toString(); if (title.contains(" - "_L1)) { QStringList title_splitted = title.split(u" - "_s); if (title_splitted.count() == 2) { - QString artist = title_splitted.first(); + const QString artist = title_splitted.first(); title = title_splitted.last(); if (artist.compare(search->artist, Qt::CaseInsensitive) != 0 && title.compare(search->album, Qt::CaseInsensitive) != 0) continue; } @@ -300,14 +303,9 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) StartReleaseRequest(search, release_id, resource_url); } - if (search->requests_release_.count() == 0) { - if (search->type == DiscogsCoverType::Master) { - search->type = DiscogsCoverType::Release; - queue_search_requests_.enqueue(search); - } - else { - EndSearch(search); - } + if (search->requests_release_.count() == 0 && search->type == DiscogsCoverType::Master) { + search->type = DiscogsCoverType::Release; + queue_search_requests_.enqueue(search); } } @@ -344,33 +342,31 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se if (!search->requests_release_.contains(release_id)) return; const DiscogsCoverReleaseContext &release = search->requests_release_.value(release_id); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - EndSearch(search, release.id); + const QScopeGuard end_search = qScopeGuard([this, search, release]() { EndSearch(search, release.id); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - EndSearch(search, release.id); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("artists"_L1) || !json_obj.contains("title"_L1)) { - Error(u"Json reply object is missing artists or title."_s, json_obj); - EndSearch(search, release.id); + if (!json_object.contains("artists"_L1) || !json_object.contains("title"_L1)) { + Error(u"Json reply object is missing artists or title."_s, json_object); return; } - if (!json_obj.contains("images"_L1)) { - EndSearch(search, release.id); + if (!json_object.contains("images"_L1)) { return; } - QJsonValue value_artists = json_obj["artists"_L1]; + const QJsonValue value_artists = json_object["artists"_L1]; if (!value_artists.isArray()) { Error(u"Json reply object artists is not a array."_s, value_artists); - EndSearch(search, release.id); return; } const QJsonArray array_artists = value_artists.toArray(); @@ -381,39 +377,35 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se Error(u"Invalid Json reply, atists array value is not a object."_s); continue; } - QJsonObject obj_artist = value_artist.toObject(); - if (!obj_artist.contains("name"_L1)) { - Error(u"Invalid Json reply, artists array value object is missing name."_s, obj_artist); + const QJsonObject object_artist = value_artist.toObject(); + if (!object_artist.contains("name"_L1)) { + Error(u"Invalid Json reply, artists array value object is missing name."_s, object_artist); continue; } - artist = obj_artist["name"_L1].toString(); + artist = object_artist["name"_L1].toString(); ++i; if (artist == search->artist) break; } if (artist.isEmpty()) { - EndSearch(search, release.id); return; } if (i > 1 && artist != search->artist) artist = "Various artists"_L1; - QString album = json_obj["title"_L1].toString(); + const QString album = json_object["title"_L1].toString(); if (artist != search->artist && album != search->album) { - EndSearch(search, release.id); return; } - QJsonValue value_images = json_obj["images"_L1]; + const QJsonValue value_images = json_object["images"_L1]; if (!value_images.isArray()) { Error(u"Json images is not an array."_s); - EndSearch(search, release.id); return; } const QJsonArray array_images = value_images.toArray(); if (array_images.isEmpty()) { Error(u"Invalid Json reply, images array is empty."_s); - EndSearch(search, release.id); return; } @@ -423,17 +415,17 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se Error(u"Invalid Json reply, images array value is not an object."_s); continue; } - QJsonObject obj_image = value_image.toObject(); + const QJsonObject obj_image = value_image.toObject(); if (!obj_image.contains("type"_L1) || !obj_image.contains("resource_url"_L1) || !obj_image.contains("width"_L1) || !obj_image.contains("height"_L1)) { Error(u"Invalid Json reply, images array value object is missing type, resource_url, width or height."_s, obj_image); continue; } - QString type = obj_image["type"_L1].toString(); + const QString type = obj_image["type"_L1].toString(); if (type != "primary"_L1) { continue; } - int width = obj_image["width"_L1].toInt(); - int height = obj_image["height"_L1].toInt(); + const int width = obj_image["width"_L1].toInt(); + const int height = obj_image["height"_L1].toInt(); if (width < 300 || height < 300) continue; const float aspect_score = static_cast(1.0) - static_cast(std::max(width, height) - std::min(width, height)) / static_cast(std::max(height, width)); if (aspect_score < 0.85) continue; @@ -448,8 +440,6 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se Q_EMIT SearchResults(search->id, search->results); search->results.clear(); - EndSearch(search, release.id); - } void DiscogsCoverProvider::EndSearch(SharedPtr search, const quint64 release_id) { diff --git a/src/covermanager/discogscoverprovider.h b/src/covermanager/discogscoverprovider.h index 8ee38133a..f5624c3c6 100644 --- a/src/covermanager/discogscoverprovider.h +++ b/src/covermanager/discogscoverprovider.h @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, Martin Björklund - * Copyright 2016-2021, Jonas Kvinge + * Copyright 2016-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,16 +24,11 @@ #include "config.h" -#include -#include -#include -#include #include #include #include #include #include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" @@ -77,9 +72,9 @@ class DiscogsCoverProvider : public JsonCoverProvider { private: void SendSearchRequest(SharedPtr search); void SendReleaseRequest(const DiscogsCoverReleaseContext &release); - QNetworkReply *CreateRequest(QUrl url, const ParamList ¶ms_provided = ParamList()); - QByteArray GetReplyData(QNetworkReply *reply); + QNetworkReply *CreateRequest(const QUrl &url, const ParamList ¶ms = ParamList()); void StartReleaseRequest(SharedPtr search, const quint64 release_id, const QUrl &url); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void EndSearch(SharedPtr search, const quint64 release_id = 0); void Error(const QString &error, const QVariant &debug = QVariant()) override; @@ -93,7 +88,6 @@ class DiscogsCoverProvider : public JsonCoverProvider { QQueue> queue_search_requests_; QQueue queue_release_requests_; QMap> requests_search_; - QList replies_; }; Q_DECLARE_METATYPE(DiscogsCoverProvider::DiscogsCoverSearchContext) diff --git a/src/covermanager/jsoncoverprovider.cpp b/src/covermanager/jsoncoverprovider.cpp index d87a6adeb..f3d8d7c30 100644 --- a/src/covermanager/jsoncoverprovider.cpp +++ b/src/covermanager/jsoncoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,10 @@ #include "config.h" -#include #include #include #include +#include #include #include "includes/shared_ptr.h" @@ -35,32 +35,31 @@ using namespace Qt::Literals::StringLiterals; JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr network, QObject *parent) : CoverProvider(name, enabled, authentication_required, quality, batch, allow_missing_album, network, parent) {} -QJsonObject JsonCoverProvider::ExtractJsonObj(const QByteArray &data) { +QJsonObject JsonCoverProvider::ExtractJsonObject(const QByteArray &data) { 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) { - Error(QStringLiteral("Failed to parse json data: %1").arg(json_error.errorString())); + Error(QStringLiteral("Failed to parse Json data: %1").arg(json_error.errorString())); return QJsonObject(); } - if (json_doc.isEmpty()) { + if (json_document.isEmpty()) { Error(u"Received empty Json document."_s, data); return QJsonObject(); } - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); + if (!json_document.isObject()) { + Error(u"Json document is not an object."_s, json_document); return QJsonObject(); } - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); + const QJsonObject json_object = json_document.object(); + if (json_object.isEmpty()) { + Error(u"Received empty Json object."_s, json_document); return QJsonObject(); } - return json_obj; + return json_object; } diff --git a/src/covermanager/jsoncoverprovider.h b/src/covermanager/jsoncoverprovider.h index e1669b52f..586a3a867 100644 --- a/src/covermanager/jsoncoverprovider.h +++ b/src/covermanager/jsoncoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ #include "config.h" -#include #include #include #include @@ -39,7 +38,7 @@ class JsonCoverProvider : public CoverProvider { explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr network, QObject *parent); protected: - QJsonObject ExtractJsonObj(const QByteArray &data); + QJsonObject ExtractJsonObject(const QByteArray &data); }; #endif // JSONCOVERPROVIDER_H diff --git a/src/covermanager/lastfmcoverprovider.cpp b/src/covermanager/lastfmcoverprovider.cpp index ae40e4e37..fc7bcde9c 100644 --- a/src/covermanager/lastfmcoverprovider.cpp +++ b/src/covermanager/lastfmcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,10 +22,7 @@ #include #include -#include #include -#include -#include #include #include #include @@ -37,6 +34,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" @@ -57,17 +55,6 @@ constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8"; LastFmCoverProvider::LastFmCoverProvider(const SharedPtr network, QObject *parent) : JsonCoverProvider(u"Last.fm"_s, true, false, 1.0, true, false, network, parent) {} -LastFmCoverProvider::~LastFmCoverProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false; @@ -111,18 +98,62 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"api_sig"_s)), QString::fromLatin1(QUrl::toPercentEncoding(signature))); url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s))); - QUrl url(QString::fromLatin1(kUrl)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8()); - replies_ << reply; + QNetworkReply *reply = CreatePostRequest(QUrl(QLatin1String(kUrl)), url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, type]() { QueryFinished(reply, id, type); }); return true; } +JsonBaseRequest::JsonObjectResult LastFmCoverProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("error"_L1) && json_object.contains("message"_L1)) { + const int error = json_object["error"_L1].toInt(); + const QString message = json_object["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(error); + } + else { + result.json_object = json_document.object(); + } + } + else { + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); + } + } + + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + return result; + +} + void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, const QString &type) { if (!replies_.contains(reply)) return; @@ -131,96 +162,85 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons reply->deleteLater(); CoverProviderSearchResults results; + const QScopeGuard end_search = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(id, results); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(id, results); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } QJsonValue value_results; - if (json_obj.contains("results"_L1)) { - value_results = json_obj["results"_L1]; + if (json_object.contains("results"_L1)) { + value_results = json_object["results"_L1]; } - else if (json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) { - int error = json_obj["error"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); + else if (json_object.contains("error"_L1) && json_object.contains("message"_L1)) { + const int error = json_object["error"_L1].toInt(); + const QString message = json_object["message"_L1].toString(); Error(QStringLiteral("Error: %1: %2").arg(QString::number(error), message)); - Q_EMIT SearchFinished(id, results); return; } else { - Error(u"Json reply is missing results."_s, json_obj); - Q_EMIT SearchFinished(id, results); + Error(u"Json reply is missing results."_s, json_object); return; } if (!value_results.isObject()) { Error(u"Json results is not a object."_s, value_results); - Q_EMIT SearchFinished(id, results); return; } - QJsonObject obj_results = value_results.toObject(); - if (obj_results.isEmpty()) { + const QJsonObject object_results = value_results.toObject(); + if (object_results.isEmpty()) { Error(u"Json results object is empty."_s, value_results); - Q_EMIT SearchFinished(id, results); return; } QJsonValue value_matches; if (type == "album"_L1) { - if (obj_results.contains("albummatches"_L1)) { - value_matches = obj_results["albummatches"_L1]; + if (object_results.contains("albummatches"_L1)) { + value_matches = object_results["albummatches"_L1]; } else { - Error(u"Json results object is missing albummatches."_s, obj_results); - Q_EMIT SearchFinished(id, results); + Error(u"Json results object is missing albummatches."_s, object_results); return; } } else if (type == "track"_L1) { - if (obj_results.contains("trackmatches"_L1)) { - value_matches = obj_results["trackmatches"_L1]; + if (object_results.contains("trackmatches"_L1)) { + value_matches = object_results["trackmatches"_L1]; } else { - Error(u"Json results object is missing trackmatches."_s, obj_results); - Q_EMIT SearchFinished(id, results); + Error(u"Json results object is missing trackmatches."_s, object_results); return; } } if (!value_matches.isObject()) { Error(u"Json albummatches or trackmatches is not an object."_s, value_matches); - Q_EMIT SearchFinished(id, results); return; } - QJsonObject obj_matches = value_matches.toObject(); - if (obj_matches.isEmpty()) { + const QJsonObject object_matches = value_matches.toObject(); + if (object_matches.isEmpty()) { Error(u"Json albummatches or trackmatches object is empty."_s, value_matches); - Q_EMIT SearchFinished(id, results); return; } - QJsonValue value_type; - if (!obj_matches.contains(type)) { - Error(QStringLiteral("Json object is missing %1.").arg(type), obj_matches); - Q_EMIT SearchFinished(id, results); + if (!object_matches.contains(type)) { + Error(QStringLiteral("Json object is missing %1.").arg(type), object_matches); return; } - value_type = obj_matches[type]; + const QJsonValue value_type = object_matches[type]; if (!value_type.isArray()) { Error(u"Json album value in albummatches object is not an array."_s, value_type); - Q_EMIT SearchFinished(id, results); return; } const QJsonArray array_type = value_type.toArray(); @@ -231,23 +251,22 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons Error(u"Invalid Json reply, value in albummatches/trackmatches array is not a object."_s); continue; } - QJsonObject obj = value.toObject(); - if (!obj.contains("artist"_L1) || !obj.contains("image"_L1) || !obj.contains("name"_L1)) { - Error(u"Invalid Json reply, album is missing artist, image or name."_s, obj); + const QJsonObject object = value.toObject(); + if (!object.contains("artist"_L1) || !object.contains("image"_L1) || !object.contains("name"_L1)) { + Error(u"Invalid Json reply, album is missing artist, image or name."_s, object); continue; } - QString artist = obj["artist"_L1].toString(); + const QString artist = object["artist"_L1].toString(); QString album; if (type == "album"_L1) { - album = obj["name"_L1].toString(); + album = object["name"_L1].toString(); } - QJsonValue json_image = obj["image"_L1]; - if (!json_image.isArray()) { - Error(u"Invalid Json reply, album image is not a array."_s, json_image); + if (!object.contains("image"_L1) || !object["image"_L1].isArray()) { + Error(u"Invalid Json reply, album image is not a array."_s, object); continue; } - const QJsonArray array_image = json_image.toArray(); + const QJsonArray array_image = object["image"_L1].toArray(); QString image_url_use; LastFmImageSize image_size_use = LastFmImageSize::Unknown; for (const QJsonValue &value_image : array_image) { @@ -255,14 +274,14 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons Error(u"Invalid Json reply, album image value is not an object."_s); continue; } - QJsonObject obj_image = value_image.toObject(); - if (!obj_image.contains("#text"_L1) || !obj_image.contains("size"_L1)) { - Error(u"Invalid Json reply, album image value is missing #text or size."_s, obj_image); + const QJsonObject object_image = value_image.toObject(); + if (!object_image.contains("#text"_L1) || !object_image.contains("size"_L1)) { + Error(u"Invalid Json reply, album image value is missing #text or size."_s, object_image); continue; } - QString image_url = obj_image["#text"_L1].toString(); + const QString image_url = object_image["#text"_L1].toString(); if (image_url.isEmpty()) continue; - LastFmImageSize image_size = ImageSizeFromString(obj_image["size"_L1].toString().toLower()); + const LastFmImageSize image_size = ImageSizeFromString(object_image["size"_L1].toString().toLower()); if (image_url_use.isEmpty() || image_size > image_size_use) { image_url_use = image_url; image_size_use = image_size; @@ -275,7 +294,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons if (image_url_use.contains("/300x300/"_L1)) { image_url_use = image_url_use.replace("/300x300/"_L1, "/740x0/"_L1); } - QUrl url(image_url_use); + const QUrl url(image_url_use); if (!url.isValid()) continue; CoverProviderSearchResult cover_result; @@ -285,50 +304,6 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons cover_result.image_size = QSize(300, 300); results << cover_result; } - Q_EMIT SearchFinished(id, results); - -} - -QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); - } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - } - else { - // See if there is Json data containing "error" and "message" - then use that instead. - data = reply->readAll(); - QString error; - 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.contains("error"_L1) && json_obj.contains("message"_L1)) { - int code = json_obj["error"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); - error = "Error: "_L1 + QString::number(code) + ": "_L1 + message; - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); - } - return QByteArray(); - } - - return data; } diff --git a/src/covermanager/lastfmcoverprovider.h b/src/covermanager/lastfmcoverprovider.h index 29a9c7c44..8b52026c3 100644 --- a/src/covermanager/lastfmcoverprovider.h +++ b/src/covermanager/lastfmcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,12 +22,8 @@ #include "config.h" -#include -#include #include -#include #include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" @@ -40,13 +36,17 @@ class LastFmCoverProvider : public JsonCoverProvider { public: explicit LastFmCoverProvider(const SharedPtr network, QObject *parent = nullptr); - ~LastFmCoverProvider() override; + + bool authentication_required() const override { return true; } bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; private Q_SLOTS: void QueryFinished(QNetworkReply *reply, const int id, const QString &type); + protected: + JsonObjectResult ParseJsonObject(QNetworkReply *reply); + private: enum class LastFmImageSize { Unknown, @@ -56,12 +56,8 @@ class LastFmCoverProvider : public JsonCoverProvider { ExtraLarge = 300 }; - QByteArray GetReplyData(QNetworkReply *reply); static LastFmImageSize ImageSizeFromString(const QString &size); void Error(const QString &error, const QVariant &debug = QVariant()) override; - - private: - QList replies_; }; #endif // LASTFMCOVERPROVIDER_H diff --git a/src/covermanager/musicbrainzcoverprovider.cpp b/src/covermanager/musicbrainzcoverprovider.cpp index 8dab15283..468c4f1d5 100644 --- a/src/covermanager/musicbrainzcoverprovider.cpp +++ b/src/covermanager/musicbrainzcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,22 +19,18 @@ #include "config.h" -#include - -#include -#include #include #include #include #include #include #include -#include #include #include #include #include #include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" @@ -62,17 +58,6 @@ MusicbrainzCoverProvider::MusicbrainzCoverProvider(const SharedPtrabort(); - reply->deleteLater(); - } - -} - bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { Q_UNUSED(title); @@ -92,19 +77,12 @@ bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString void MusicbrainzCoverProvider::SendSearchRequest(const SearchRequest &request) { - QString query = QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace(u'"', "\""_L1), request.artist.trimmed().replace(u'"', "\""_L1)); - + const QString query = QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace(u'"', "\""_L1), request.artist.trimmed().replace(u'"', "\""_L1)); QUrlQuery url_query; url_query.addQueryItem(u"query"_s, query); url_query.addQueryItem(u"limit"_s, QString::number(kLimit)); url_query.addQueryItem(u"fmt"_s, u"json"_s); - - QUrl url(QString::fromLatin1(kReleaseSearchUrl)); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kReleaseSearchUrl)), url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { HandleSearchReply(reply, request.id); }); } @@ -120,6 +98,55 @@ void MusicbrainzCoverProvider::FlushRequests() { } +JsonBaseRequest::JsonObjectResult MusicbrainzCoverProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + } + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("error"_L1) && json_object.contains("help"_L1)) { + const QString error = json_object["error"_L1].toString(); + const QString help = json_object["help"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(error, help); + } + else { + result.json_object = json_document.object(); + } + } + else { + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); + } + } + + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + return result; + +} + void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int search_id) { if (!replies_.contains(reply)) return; @@ -128,41 +155,33 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int reply->deleteLater(); CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, search_id, &results]() { Q_EMIT SearchFinished(search_id, results); }); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(search_id, results); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(search_id, results); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("releases"_L1)) { - if (json_obj.contains("error"_L1)) { - QString error = json_obj["error"_L1].toString(); - Error(error); - } - else { - Error(u"Json reply is missing releases."_s, json_obj); - } - Q_EMIT SearchFinished(search_id, results); + if (!json_object.contains("releases"_L1)) { + Error(u"Json reply is missing releases."_s, json_object); return; } - QJsonValue value_releases = json_obj["releases"_L1]; + + const QJsonValue value_releases = json_object["releases"_L1]; if (!value_releases.isArray()) { Error(u"Json releases is not an array."_s, value_releases); - Q_EMIT SearchFinished(search_id, results); return; } const QJsonArray array_releases = value_releases.toArray(); if (array_releases.isEmpty()) { - Q_EMIT SearchFinished(search_id, results); return; } @@ -172,18 +191,18 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int Error(u"Invalid Json reply, releases array value is not an object."_s); continue; } - QJsonObject obj_release = value_release.toObject(); - if (!obj_release.contains("id"_L1) || !obj_release.contains("artist-credit"_L1) || !obj_release.contains("title"_L1)) { - Error(u"Invalid Json reply, releases array object is missing id, artist-credit or title."_s, obj_release); + const QJsonObject object_release = value_release.toObject(); + if (!object_release.contains("id"_L1) || !object_release.contains("artist-credit"_L1) || !object_release.contains("title"_L1)) { + Error(u"Invalid Json reply, releases array object is missing id, artist-credit or title."_s, object_release); continue; } - QJsonValue json_artists = obj_release["artist-credit"_L1]; - if (!json_artists.isArray()) { - Error(u"Invalid Json reply, artist-credit is not a array."_s, json_artists); + const QJsonValue value_artists = object_release["artist-credit"_L1]; + if (!value_artists.isArray()) { + Error(u"Invalid Json reply, artist-credit is not a array."_s, value_artists); continue; } - const QJsonArray array_artists = json_artists.toArray(); + const QJsonArray array_artists = value_artists.toArray(); int i = 0; QString artist; for (const QJsonValue &value_artist : array_artists) { @@ -191,18 +210,18 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int Error(u"Invalid Json reply, artist is not a object."_s); continue; } - QJsonObject obj_artist = value_artist.toObject(); + const QJsonObject object_artist = value_artist.toObject(); - if (!obj_artist.contains("artist"_L1)) { - Error(u"Invalid Json reply, artist is missing."_s, obj_artist); + if (!object_artist.contains("artist"_L1)) { + Error(u"Invalid Json reply, artist is missing."_s, object_artist); continue; } - QJsonValue value_artist2 = obj_artist["artist"_L1]; + const QJsonValue value_artist2 = object_artist["artist"_L1]; if (!value_artist2.isObject()) { Error(u"Invalid Json reply, artist is not an object."_s, value_artist2); continue; } - QJsonObject obj_artist2 = value_artist2.toObject(); + const QJsonObject obj_artist2 = value_artist2.toObject(); if (!obj_artist2.contains("name"_L1)) { Error(u"Invalid Json reply, artist is missing name."_s, value_artist2); @@ -213,59 +232,16 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int } if (i > 1) artist = "Various artists"_L1; - QString id = obj_release["id"_L1].toString(); - QString album = obj_release["title"_L1].toString(); + const QString id = object_release["id"_L1].toString(); + const QString album = object_release["title"_L1].toString(); CoverProviderSearchResult cover_result; - QUrl url(QString::fromLatin1(kAlbumCoverUrl).arg(id)); + const QUrl url(QString::fromLatin1(kAlbumCoverUrl).arg(id)); cover_result.artist = artist; cover_result.album = album; cover_result.image_url = url; results.append(cover_result); } - Q_EMIT SearchFinished(search_id, results); - -} - -QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); - } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - QString failure_reason = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); - } - else { - // See if there is Json data containing "error" - then use that instead. - data = reply->readAll(); - QString error; - 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.contains("error"_L1)) { - error = json_obj["error"_L1].toString(); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); - } - return QByteArray(); - } - - return data; } diff --git a/src/covermanager/musicbrainzcoverprovider.h b/src/covermanager/musicbrainzcoverprovider.h index 1b9b648de..962358348 100644 --- a/src/covermanager/musicbrainzcoverprovider.h +++ b/src/covermanager/musicbrainzcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,13 +22,9 @@ #include "config.h" -#include -#include #include -#include #include #include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" @@ -42,7 +38,6 @@ class MusicbrainzCoverProvider : public JsonCoverProvider { public: explicit MusicbrainzCoverProvider(const SharedPtr network, QObject *parent = nullptr); - ~MusicbrainzCoverProvider() override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; @@ -59,13 +54,12 @@ class MusicbrainzCoverProvider : public JsonCoverProvider { }; void SendSearchRequest(const SearchRequest &request); - QByteArray GetReplyData(QNetworkReply *reply); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void Error(const QString &error, const QVariant &debug = QVariant()) override; private: QTimer *timer_flush_requests_; QQueue queue_search_requests_; - QList replies_; }; #endif // MUSICBRAINZCOVERPROVIDER_H diff --git a/src/covermanager/musixmatchcoverprovider.cpp b/src/covermanager/musixmatchcoverprovider.cpp index f1a0d4e9a..c426d1af1 100644 --- a/src/covermanager/musixmatchcoverprovider.cpp +++ b/src/covermanager/musixmatchcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * 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,20 +19,20 @@ #include "config.h" -#include #include #include #include #include #include #include -#include #include #include #include #include +#include #include "includes/shared_ptr.h" +#include "utilities/musixmatchprovider.h" #include "core/logging.h" #include "core/networkaccessmanager.h" #include "albumcoverfetcher.h" @@ -40,37 +40,23 @@ #include "musixmatchcoverprovider.h" using namespace Qt::Literals::StringLiterals; +using namespace MusixmatchProvider; MusixmatchCoverProvider::MusixmatchCoverProvider(const SharedPtr network, QObject *parent) : JsonCoverProvider(u"Musixmatch"_s, true, false, 1.0, true, false, network, parent) {} -MusixmatchCoverProvider::~MusixmatchCoverProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - bool MusixmatchCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { Q_UNUSED(title); if (artist.isEmpty() || album.isEmpty()) return false; - QString artist_stripped = StringFixup(artist); - QString album_stripped = StringFixup(album); + const QString artist_stripped = StringFixup(artist); + const QString album_stripped = StringFixup(album); if (artist_stripped.isEmpty() || album_stripped.isEmpty()) return false; - QUrl url(QStringLiteral("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped, album_stripped)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QStringLiteral("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped, album_stripped))); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, artist, album]() { HandleSearchReply(reply, id, artist, album); }); //qLog(Debug) << "Musixmatch: Sending request for" << artist_stripped << album_stripped << url; @@ -89,29 +75,19 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int reply->deleteLater(); CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results);; }); - if (reply->error() != QNetworkReply::NoError) { - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - Q_EMIT SearchFinished(id, results); - return; - } - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - Q_EMIT SearchFinished(id, results); + const ReplyDataResult reply_data_result = GetReplyData(reply); + if (!reply_data_result.success()) { + Error(reply_data_result.error_message); return; } - const QByteArray data = reply->readAll(); - if (data.isEmpty()) { - Error(u"Empty reply received from server."_s); - Q_EMIT SearchFinished(id, results); - return; - } + const QByteArray &data = reply_data_result.data; const QString content = QString::fromUtf8(data); const QString data_begin = ""_L1; if (!content.contains(data_begin) || !content.contains(data_end)) { - Q_EMIT SearchFinished(id, results); return; } qint64 begin_idx = content.indexOf(data_begin); @@ -125,89 +101,61 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int } if (content_json.isEmpty()) { - Q_EMIT SearchFinished(id, results); return; } static const QRegularExpression regex_html_tag(u"<[^>]*>"_s); if (content_json.contains(regex_html_tag)) { // Make sure it's not HTML code. - Q_EMIT SearchFinished(id, results); return; } - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(content_json.toUtf8(), &error); - - if (error.error != QJsonParseError::NoError) { - Error(QStringLiteral("Failed to parse json data: %1").arg(error.errorString())); - Q_EMIT SearchFinished(id, results); + const JsonObjectResult json_object_result = GetJsonObject(content_json.toUtf8()); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - if (json_doc.isEmpty()) { - Error(u"Received empty Json document."_s, data); - Q_EMIT SearchFinished(id, results); + QJsonObject json_object = json_object_result.json_object; + if (!json_object.contains("props"_L1) || !json_object["props"_L1].isObject()) { + Error(u"Json reply is missing props."_s, json_object); return; } + json_object = json_object["props"_L1].toObject(); - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); + if (!json_object.contains("pageProps"_L1) || !json_object["pageProps"_L1].isObject()) { + Error(u"Json props is missing pageProps."_s, json_object); Q_EMIT SearchFinished(id, results); return; } + json_object = json_object["pageProps"_L1].toObject(); - QJsonObject obj_data = json_doc.object(); - if (obj_data.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); - Q_EMIT SearchFinished(id, results); + if (!json_object.contains("data"_L1) || !json_object["data"_L1].isObject()) { + Error(u"Json pageProps is missing data."_s, json_object); return; } + json_object = json_object["data"_L1].toObject(); - if (!obj_data.contains("props"_L1) || !obj_data["props"_L1].isObject()) { - Error(u"Json reply is missing props."_s, obj_data); - Q_EMIT SearchFinished(id, results); + if (!json_object.contains("albumGet"_L1) || !json_object["albumGet"_L1].isObject()) { + Error(u"Json data is missing albumGet."_s, json_object); return; } - obj_data = obj_data["props"_L1].toObject(); + json_object = json_object["albumGet"_L1].toObject(); - if (!obj_data.contains("pageProps"_L1) || !obj_data["pageProps"_L1].isObject()) { - Error(u"Json props is missing pageProps."_s, obj_data); - Q_EMIT SearchFinished(id, results); + if (!json_object.contains("data"_L1) || !json_object["data"_L1].isObject()) { + Error(u"Json albumGet reply is missing data."_s, json_object); return; } - obj_data = obj_data["pageProps"_L1].toObject(); - - if (!obj_data.contains("data"_L1) || !obj_data["data"_L1].isObject()) { - Error(u"Json pageProps is missing data."_s, obj_data); - Q_EMIT SearchFinished(id, results); - return; - } - obj_data = obj_data["data"_L1].toObject(); - - if (!obj_data.contains("albumGet"_L1) || !obj_data["albumGet"_L1].isObject()) { - Error(u"Json data is missing albumGet."_s, obj_data); - Q_EMIT SearchFinished(id, results); - return; - } - obj_data = obj_data["albumGet"_L1].toObject(); - - if (!obj_data.contains("data"_L1) || !obj_data["data"_L1].isObject()) { - Error(u"Json albumGet reply is missing data."_s, obj_data); - Q_EMIT SearchFinished(id, results); - return; - } - obj_data = obj_data["data"_L1].toObject(); + json_object = json_object["data"_L1].toObject(); CoverProviderSearchResult result; - if (obj_data.contains("artistName"_L1) && obj_data["artistName"_L1].isString()) { - result.artist = obj_data["artistName"_L1].toString(); + if (json_object.contains("artistName"_L1) && json_object["artistName"_L1].isString()) { + result.artist = json_object["artistName"_L1].toString(); } - if (obj_data.contains("name"_L1) && obj_data["name"_L1].isString()) { - result.album = obj_data["name"_L1].toString(); + if (json_object.contains("name"_L1) && json_object["name"_L1].isString()) { + result.album = json_object["name"_L1].toString(); } if (result.artist.compare(artist, Qt::CaseInsensitive) != 0 && result.album.compare(album, Qt::CaseInsensitive) != 0) { - Q_EMIT SearchFinished(id, results); return; } @@ -216,8 +164,8 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int << qMakePair(u"coverImage350x350"_s, QSize(350, 350)); for (const QPair &cover_size : cover_sizes) { - if (!obj_data.contains(cover_size.first)) continue; - QUrl cover_url(obj_data[cover_size.first].toString()); + if (!json_object.contains(cover_size.first)) continue; + const QUrl cover_url(json_object[cover_size.first].toString()); if (cover_url.isValid()) { result.image_url = cover_url; result.image_size = cover_size.second; @@ -225,8 +173,6 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int } } - Q_EMIT SearchFinished(id, results); - } void MusixmatchCoverProvider::Error(const QString &error, const QVariant &debug) { diff --git a/src/covermanager/musixmatchcoverprovider.h b/src/covermanager/musixmatchcoverprovider.h index 52554b061..fd45a5487 100644 --- a/src/covermanager/musixmatchcoverprovider.h +++ b/src/covermanager/musixmatchcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,25 +22,20 @@ #include "config.h" -#include -#include -#include #include #include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" -#include "providers/musixmatchprovider.h" class QNetworkReply; class NetworkAccessManager; -class MusixmatchCoverProvider : public JsonCoverProvider, MusixmatchProvider { +class MusixmatchCoverProvider : public JsonCoverProvider { Q_OBJECT public: explicit MusixmatchCoverProvider(const SharedPtr network, QObject *parent = nullptr); - ~MusixmatchCoverProvider() override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; @@ -50,9 +45,6 @@ class MusixmatchCoverProvider : public JsonCoverProvider, MusixmatchProvider { private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album); - - private: - QList replies_; }; #endif // MUSIXMATCHCOVERPROVIDER_H diff --git a/src/covermanager/opentidalcoverprovider.cpp b/src/covermanager/opentidalcoverprovider.cpp index bccc032bf..e2478caef 100644 --- a/src/covermanager/opentidalcoverprovider.cpp +++ b/src/covermanager/opentidalcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * 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,11 +19,6 @@ #include "config.h" -#include - -#include -#include -#include #include #include #include @@ -36,13 +31,13 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" #include "core/logging.h" -#include "core/settings.h" #include "core/song.h" -#include "constants/timeconstants.h" +#include "core/oauthenticator.h" #include "albumcoverfetcher.h" #include "jsoncoverprovider.h" #include "opentidalcoverprovider.h" @@ -51,8 +46,8 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr char kSettingsGroup[] = "OpenTidal"; -constexpr char kAuthUrl[] = "https://auth.tidal.com/v1/oauth2/token"; -constexpr char kApiUrl[] = "https://openapi.tidal.com"; +constexpr char kOAuthAccessTokenUrl[] = "https://auth.tidal.com/v1/oauth2/token"; +constexpr char kApiUrl[] = "https://openapi.tidal.com/v2"; constexpr char kApiClientIdB64[] = "RHBwV3FpTEM4ZFJSV1RJaQ=="; constexpr char kApiClientSecretB64[] = "cGk0QmxpclZXQWlteWpBc0RnWmZ5RmVlRzA2b3E1blVBVTljUW1IdFhDST0="; constexpr int kLimit = 10; @@ -63,32 +58,24 @@ using std::make_shared; OpenTidalCoverProvider::OpenTidalCoverProvider(const SharedPtr network, QObject *parent) : JsonCoverProvider(u"OpenTidal"_s, true, false, 2.5, true, false, network, parent), - login_timer_(new QTimer(this)), + oauth_(new OAuthenticator(network, this)), timer_flush_requests_(new QTimer(this)), - login_in_progress_(false), - have_login_(false), - login_time_(0), - expires_in_(0) { + login_in_progress_(false) { - login_timer_->setSingleShot(true); - QObject::connect(login_timer_, &QTimer::timeout, this, &OpenTidalCoverProvider::Login); + oauth_->set_settings_group(QLatin1String(kSettingsGroup)); + oauth_->set_type(OAuthenticator::Type::Client_Credentials); + oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl))); + oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kApiClientIdB64))); + oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kApiClientSecretB64))); + oauth_->set_use_local_redirect_server(false); + oauth_->set_random_port(false); + QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &OpenTidalCoverProvider::OAuthFinished); timer_flush_requests_->setInterval(kRequestsDelay); timer_flush_requests_->setSingleShot(false); QObject::connect(timer_flush_requests_, &QTimer::timeout, this, &OpenTidalCoverProvider::FlushRequests); - LoadSession(); - -} - -OpenTidalCoverProvider::~OpenTidalCoverProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } + oauth_->LoadSession(); } @@ -96,7 +83,7 @@ bool OpenTidalCoverProvider::StartSearch(const QString &artist, const QString &a if (artist.isEmpty() || album.isEmpty()) return false; - if (!have_login_ && !login_in_progress_ && QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() < 120) { + if (!oauth_->authenticated() && !login_in_progress_ && (last_login_attempt_.isValid() && (QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch()) < 120)) { return false; } @@ -115,32 +102,10 @@ void OpenTidalCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); } -void OpenTidalCoverProvider::LoadSession() { - - Settings s; - s.beginGroup(kSettingsGroup); - token_type_ = s.value("token_type").toString(); - access_token_ = s.value("access_token").toString(); - expires_in_ = s.value("expires_in", 0).toLongLong(); - login_time_ = s.value("login_time", 0).toLongLong(); - s.endGroup(); - - if (!token_type_.isEmpty() && !access_token_.isEmpty() && (login_time_ + expires_in_) > (QDateTime::currentSecsSinceEpoch() + 30)) { - have_login_ = true; - } - -} - void OpenTidalCoverProvider::FlushRequests() { - if (have_login_ && (login_time_ + expires_in_) < QDateTime::currentSecsSinceEpoch()) { - have_login_ = false; - } - - if (!have_login_) { - if (!login_in_progress_) { - Login(); - } + if (!oauth_->authenticated()) { + LoginCheck(); return; } @@ -155,7 +120,7 @@ void OpenTidalCoverProvider::FlushRequests() { void OpenTidalCoverProvider::LoginCheck() { - if (!login_in_progress_ && (!last_login_attempt_.isValid() || QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() > 120)) { + if (!oauth_->authenticated() && !login_in_progress_ && (!last_login_attempt_.isValid() || QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() > 120)) { Login(); } @@ -165,156 +130,93 @@ void OpenTidalCoverProvider::Login() { qLog(Debug) << "Authenticating..."; - if (login_timer_->isActive()) { - login_timer_->stop(); - } - - have_login_ = false; login_in_progress_ = true; - last_login_attempt_ = QDateTime::currentDateTime(); - QUrl url(QString::fromLatin1(kAuthUrl)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setRawHeader("Authorization", "Basic " + QByteArray(QByteArray::fromBase64(kApiClientIdB64) + ":" + QByteArray::fromBase64(kApiClientSecretB64)).toBase64()); - QUrlQuery url_query; - url_query.addQueryItem(u"grant_type"_s, u"client_credentials"_s); - QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8()); - replies_ << reply; - QObject::connect(reply, &QNetworkReply::sslErrors, this, &OpenTidalCoverProvider::HandleLoginSSLErrors); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { LoginFinished(reply); }); + oauth_->Authenticate(); } -void OpenTidalCoverProvider::HandleLoginSSLErrors(const QList &ssl_errors) { - - for (const QSslError &ssl_error : ssl_errors) { - qLog(Error) << "OpenTidal:" << ssl_error.errorString(); - } - -} - -void OpenTidalCoverProvider::LoginFinished(QNetworkReply *reply) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->deleteLater(); +void OpenTidalCoverProvider::OAuthFinished(const bool success, const QString &error) { login_in_progress_ = false; - last_login_attempt_ = QDateTime(); - QJsonObject json_obj = GetJsonObject(reply); - if (json_obj.isEmpty()) { - FinishAllSearches(); - return; + if (success) { + qLog(Debug) << "OpenTidal: Authentication successful"; + last_login_attempt_ = QDateTime(); + if (!timer_flush_requests_->isActive()) { + timer_flush_requests_->start(); + } } - - if (!json_obj.contains("access_token"_L1) || - !json_obj.contains("token_type"_L1) || - !json_obj.contains("expires_in"_L1) || - !json_obj["access_token"_L1].isString() || - !json_obj["token_type"_L1].isString()) { - qLog(Error) << "OpenTidal: Invalid login reply."; - FinishAllSearches(); - return; + else { + qLog(Debug) << "OpenTidal: Authentication failed" << error; + last_login_attempt_ = QDateTime::currentDateTime(); } - have_login_ = true; - token_type_ = json_obj["token_type"_L1].toString(); - access_token_ = json_obj["access_token"_L1].toString(); - login_time_ = QDateTime::currentSecsSinceEpoch(); - expires_in_ = json_obj["expires_in"_L1].toInt(); - - Settings s; - s.beginGroup(kSettingsGroup); - s.setValue("token_type", token_type_); - s.setValue("access_token", access_token_); - s.setValue("expires_in", expires_in_); - s.setValue("login_time", login_time_); - s.endGroup(); - - if (expires_in_ <= 300) { - expires_in_ = 300; - } - - expires_in_ -= 30; - - login_timer_->setInterval(static_cast(expires_in_ * kMsecPerSec)); - login_timer_->start(); - - if (!timer_flush_requests_->isActive()) { - timer_flush_requests_->start(); - } - - qLog(Debug) << "Authentication successful"; - } -QJsonObject OpenTidalCoverProvider::ExtractJsonObj(const QByteArray &data) { +JsonBaseRequest::JsonObjectResult OpenTidalCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QJsonParseError json_parse_error; - const QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_parse_error); - if (json_parse_error.error != QJsonParseError::NoError) { - qLog(Error) << "OpenTidal:" << json_parse_error.errorString(); - return QJsonObject(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - if (!json_doc.isObject()) { - return QJsonObject(); - } - return json_doc.object(); -} - -QJsonObject OpenTidalCoverProvider::GetJsonObject(QNetworkReply *reply) { - - const int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (reply->error() != QNetworkReply::NoError || (http_code != 200 && http_code != 207)) { - if (reply->error() != QNetworkReply::NoError) { - qLog(Error) << "OpenTidal:" << reply->errorString(); - if (reply->error() < 200) { - return QJsonObject(); - } - if (reply->error() == QNetworkReply::AuthenticationRequiredError) { - LoginCheck(); - } - } - else if (http_code != 200 && http_code != 207) { - qLog(Error) << "OpenTidal: Received HTTP code" << http_code; - } - const QByteArray data = reply->readAll(); - if (data.isEmpty()) { - return QJsonObject(); - } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.contains("errors"_L1) && json_obj["errors"_L1].isArray()) { - const QJsonArray array = json_obj["errors"_L1].toArray(); - for (const QJsonValue &value : array) { - if (!value.isObject()) continue; - QJsonObject obj = value.toObject(); - if (!obj.contains("category"_L1) || - !obj.contains("code"_L1) || - !obj.contains("detail"_L1)) { - continue; - } - QString category = obj["category"_L1].toString(); - QString code = obj["code"_L1].toString(); - QString detail = obj["detail"_L1].toString(); - qLog(Error) << "OpenTidal:" << category << code << detail; - if (category == "AUTHENTICATION_ERROR"_L1) { - LoginCheck(); - } - } - } - return QJsonObject(); + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); } const QByteArray data = reply->readAll(); - if (data.isEmpty()) { - return QJsonObject(); + bool clear_session = false; + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("errors"_L1) && json_object["errors"_L1].isArray()) { + const QJsonArray array_errors = json_object["errors"_L1].toArray(); + for (const auto &value : array_errors) { + if (!value.isObject()) continue; + const QJsonObject object_error = value.toObject(); + if (!object_error.contains("category"_L1) || !object_error.contains("code"_L1) || !object_error.contains("detail"_L1)) { + continue; + } + const QString category = object_error["category"_L1].toString(); + const QString code = object_error["code"_L1].toString(); + const QString detail = object_error["detail"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2) (%3)").arg(category, code, detail); + if (category == "AUTHENTICATION_ERROR"_L1) { + clear_session = true; + } + } + } + else { + result.json_object = json_document.object(); + } + } + else { + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); + } } - return ExtractJsonObj(data); + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError || clear_session) { + oauth_->ClearSession(); + } + + return result; } @@ -331,17 +233,19 @@ void OpenTidalCoverProvider::SendSearchRequest(SearchRequestPtr search_request) } QUrlQuery url_query; - url_query.addQueryItem(u"query"_s, QString::fromUtf8(QUrl::toPercentEncoding(query))); + url_query.addQueryItem(u"include"_s, u"albums"_s); url_query.addQueryItem(u"limit"_s, QString::number(kLimit)); url_query.addQueryItem(u"countryCode"_s, u"US"_s); - QUrl url(QLatin1String(kApiUrl) + "/search"_L1); + QUrl url(QLatin1String(kApiUrl) + "/searchresults/"_L1 + QString::fromUtf8(QUrl::toPercentEncoding(query))); url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/vnd.tidal.v1+json"_s); - req.setRawHeader("Authorization", token_type_.toUtf8() + " " + access_token_.toUtf8()); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/vnd.tidal.v1+json"_s); + if (oauth_->authenticated()) { + network_request.setRawHeader("Authorization", oauth_->authorization_header()); + } - QNetworkReply *reply = network_->get(req); + QNetworkReply *reply = network_->get(network_request); replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search_request]() { HandleSearchReply(reply, search_request); }); @@ -354,92 +258,76 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QJsonObject json_obj = GetJsonObject(reply); - if (json_obj.isEmpty()) { + CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, search_request, &results]() { Q_EMIT SearchFinished(search_request->id, results); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); if (login_in_progress_) { search_requests_queue_.prepend(search_request); } - else { - Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults()); - } + return; + } + const QJsonObject &json_object = json_object_result.json_object; + + if (!json_object.contains("included"_L1) || !json_object["included"_L1].isArray()) { + qLog(Error) << "OpenTidal: Json object is missing included."; return; } - if (!json_obj.contains("albums"_L1) || !json_obj["albums"_L1].isArray()) { - qLog(Debug) << "OpenTidal: Json object is missing albums."; - Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults()); + const QJsonArray array_included = json_object["included"_L1].toArray(); + if (array_included.isEmpty()) { return; } - const QJsonArray array_albums = json_obj["albums"_L1].toArray(); - if (array_albums.isEmpty()) { - Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults()); - return; - } - - CoverProviderSearchResults results; int i = 0; - for (const QJsonValue &value_album : array_albums) { + for (const auto &value_included : array_included) { - if (!value_album.isObject()) { - qLog(Debug) << "OpenTidal: Invalid Json reply: Albums array value is not a object."; + if (!value_included.isObject()) { + qLog(Error) << "OpenTidal: Invalid Json reply: Albums array value is not a object."; continue; } - QJsonObject obj_album = value_album.toObject(); + const QJsonObject object_included = value_included.toObject(); - if (!obj_album.contains("resource"_L1) || !obj_album["resource"_L1].isObject()) { - qLog(Debug) << "OpenTidal: Invalid Json reply: Albums array album is missing resource object."; + if (!object_included.contains("attributes"_L1) || !object_included["attributes"_L1].isObject()) { + qLog(Error) << "OpenTidal: Invalid Json reply: Included array item is missing attributes object." << object_included; continue; } - QJsonObject obj_resource = obj_album["resource"_L1].toObject(); + const QJsonObject object_attributes = object_included["attributes"_L1].toObject(); - if (!obj_resource.contains("artists"_L1) || !obj_resource["artists"_L1].isArray()) { - qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing artists array."; + if (!object_attributes.contains("title"_L1) || !object_attributes["title"_L1].isString()) { + qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing title string." << object_attributes; continue; } - if (!obj_resource.contains("title"_L1) || !obj_resource["title"_L1].isString()) { - qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing title."; + if (!object_attributes.contains("imageLinks"_L1) || !object_attributes["imageLinks"_L1].isArray()) { + qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing imageLinks object." << object_attributes; continue; } - if (!obj_resource.contains("imageCover"_L1) || !obj_resource["imageCover"_L1].isArray()) { - qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing imageCover array."; - continue; - } + const QString album = object_attributes["title"_L1].toString(); + const QJsonArray array_imagelinks = object_attributes["imageLinks"_L1].toArray(); - QString artist; - const QString album = obj_resource["title"_L1].toString(); - - const QJsonArray array_artists = obj_resource["artists"_L1].toArray(); - for (const QJsonValue &value_artist : array_artists) { - if (!value_artist.isObject()) { + for (const auto &value_imagelink : array_imagelinks) { + if (!value_imagelink.isObject()) { continue; } - QJsonObject obj_artist = value_artist.toObject(); - if (!obj_artist.contains("name"_L1)) { + const QJsonObject object_imagelink = value_imagelink.toObject(); + if (!object_imagelink.contains("href"_L1) || !object_imagelink.contains("meta"_L1) || !object_imagelink["meta"_L1].isObject()) { continue; } - artist = obj_artist["name"_L1].toString(); - break; - } - - const QJsonArray array_covers = obj_resource["imageCover"_L1].toArray(); - for (const QJsonValue &value_cover : array_covers) { - if (!value_cover.isObject()) { + QJsonObject object_meta = object_imagelink["meta"_L1].toObject(); + if (!object_meta.contains("width"_L1) || !object_meta.contains("height"_L1)) { continue; } - QJsonObject obj_cover = value_cover.toObject(); - if (!obj_cover.contains("url"_L1) || !obj_cover.contains("width"_L1) || !obj_cover.contains("height"_L1)) { - continue; - } - const QUrl url(obj_cover["url"_L1].toString()); - const int width = obj_cover["width"_L1].toInt(); - const int height = obj_cover["height"_L1].toInt(); + const QUrl url(object_imagelink["href"_L1].toString()); + const int width = object_meta["width"_L1].toInt(); + const int height = object_meta["height"_L1].toInt(); if (!url.isValid()) continue; if (width < 640 || height < 640) continue; CoverProviderSearchResult cover_result; - cover_result.artist = artist; + cover_result.artist = search_request->artist; cover_result.album = Song::AlbumRemoveDiscMisc(album); cover_result.image_url = url; cover_result.image_size = QSize(width, height); @@ -448,8 +336,6 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque } } - Q_EMIT SearchFinished(search_request->id, results); - } void OpenTidalCoverProvider::FinishAllSearches() { @@ -465,7 +351,7 @@ void OpenTidalCoverProvider::FinishAllSearches() { void OpenTidalCoverProvider::Error(const QString &error, const QVariant &debug) { - qLog(Error) << "Tidal:" << error; + qLog(Error) << "OpenTidal:" << error; if (debug.isValid()) qLog(Debug) << debug; } diff --git a/src/covermanager/opentidalcoverprovider.h b/src/covermanager/opentidalcoverprovider.h index 906f9a0e2..030939c0f 100644 --- a/src/covermanager/opentidalcoverprovider.h +++ b/src/covermanager/opentidalcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2024, Jonas Kvinge + * Copyright 2024-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,15 +22,10 @@ #include "config.h" -#include -#include #include #include -#include #include #include -#include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" @@ -38,13 +33,13 @@ class QNetworkReply; class NetworkAccessManager; class QTimer; +class OAuthenticator; class OpenTidalCoverProvider : public JsonCoverProvider { Q_OBJECT public: explicit OpenTidalCoverProvider(const SharedPtr network, QObject *parent = nullptr); - ~OpenTidalCoverProvider() override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; @@ -60,33 +55,24 @@ class OpenTidalCoverProvider : public JsonCoverProvider { using SearchRequestPtr = SharedPtr; private: - void LoadSession(); void LoginCheck(); void Login(); - QJsonObject GetJsonObject(QNetworkReply *reply); - QJsonObject ExtractJsonObj(const QByteArray &data); void SendSearchRequest(SearchRequestPtr request); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void FinishAllSearches(); void Error(const QString &error, const QVariant &debug = QVariant()) override; private Q_SLOTS: + void OAuthFinished(const bool success, const QString &error = QString()); void FlushRequests(); - void LoginFinished(QNetworkReply *reply); - void HandleLoginSSLErrors(const QList &ssl_errors); void HandleSearchReply(QNetworkReply *reply, OpenTidalCoverProvider::SearchRequestPtr search_request); private: - QTimer *login_timer_; + OAuthenticator *oauth_; QTimer *timer_flush_requests_; bool login_in_progress_; QDateTime last_login_attempt_; - bool have_login_; - QString token_type_; - QString access_token_; - qint64 login_time_; - qint64 expires_in_; QQueue search_requests_queue_; - QList replies_; }; #endif // OPENTIDALCOVERPROVIDER_H diff --git a/src/covermanager/qobuzcoverprovider.cpp b/src/covermanager/qobuzcoverprovider.cpp index 1cc5145c1..68216b707 100644 --- a/src/covermanager/qobuzcoverprovider.cpp +++ b/src/covermanager/qobuzcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +22,6 @@ #include #include -#include -#include #include #include #include @@ -35,6 +33,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" @@ -49,26 +48,27 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr int kLimit = 10; -} +} // namespace QobuzCoverProvider::QobuzCoverProvider(const QobuzServicePtr service, SharedPtr network, QObject *parent) : JsonCoverProvider(u"Qobuz"_s, true, true, 2.0, true, true, network, parent), service_(service) {} -QobuzCoverProvider::~QobuzCoverProvider() { +bool QobuzCoverProvider::authenticated() const { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } + return service_->authenticated(); + +} + +void QobuzCoverProvider::ClearSession() { + + service_->ClearSession(); } bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { - if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false; + if (!service_->authenticated() || (artist.isEmpty() && album.isEmpty() && title.isEmpty())) return false; QString resource; QString query = artist; @@ -99,12 +99,12 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album QUrl url(QLatin1String(QobuzService::kApiUrl) + QLatin1Char('/') + resource); url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - req.setRawHeader("X-App-Id", service_->app_id().toUtf8()); - req.setRawHeader("X-User-Auth-Token", service_->user_auth_token().toUtf8()); - QNetworkReply *reply = network_->get(req); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + network_request.setRawHeader("X-App-Id", service_->app_id().toUtf8()); + network_request.setRawHeader("X-User-Auth-Token", service_->user_auth_token().toUtf8()); + QNetworkReply *reply = network_->get(network_request); replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); }); @@ -114,46 +114,56 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album void QobuzCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); } -QByteArray QobuzCoverProvider::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult QobuzCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("code"_L1) && json_object.contains("status"_L1) && json_object.contains("message"_L1)) { + const int code = json_object["code"_L1].toInt(); + const QString message = json_object["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(code); + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "status", "code" and "message" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - if (parse_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(); - error = QStringLiteral("%1 (%2)").arg(message).arg(code); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + service_->ClearSession(); + } + + return result; } @@ -165,49 +175,45 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { reply->deleteLater(); CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(id, results); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(id, results); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } QJsonValue value_type; - if (json_obj.contains("albums"_L1)) { - value_type = json_obj["albums"_L1]; + if (json_object.contains("albums"_L1)) { + value_type = json_object["albums"_L1]; } - else if (json_obj.contains("tracks"_L1)) { - value_type = json_obj["tracks"_L1]; + else if (json_object.contains("tracks"_L1)) { + value_type = json_object["tracks"_L1]; } else { - Error(u"Json reply is missing albums and tracks object."_s, json_obj); - Q_EMIT SearchFinished(id, results); + Error(u"Json reply is missing albums and tracks object."_s, json_object); return; } if (!value_type.isObject()) { Error(u"Json albums or tracks is not a object."_s, value_type); - Q_EMIT SearchFinished(id, results); return; } - QJsonObject obj_type = value_type.toObject(); + const QJsonObject object_type = value_type.toObject(); - if (!obj_type.contains("items"_L1)) { - Error(u"Json albums or tracks object does not contain items."_s, obj_type); - Q_EMIT SearchFinished(id, results); + if (!object_type.contains("items"_L1)) { + Error(u"Json albums or tracks object does not contain items."_s, object_type); return; } - QJsonValue value_items = obj_type["items"_L1]; + const QJsonValue value_items = object_type["items"_L1]; if (!value_items.isArray()) { Error(u"Json albums or track object items is not a array."_s, value_items); - Q_EMIT SearchFinished(id, results); return; } const QJsonArray array_items = value_items.toArray(); @@ -218,52 +224,52 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { Error(u"Invalid Json reply, value in items is not a object."_s); continue; } - QJsonObject item_obj = value.toObject(); + const QJsonObject item_object = value.toObject(); - QJsonObject obj_album; - if (item_obj.contains("album"_L1)) { - if (!item_obj["album"_L1].isObject()) { - Error(u"Invalid Json reply, items album is not a object."_s, item_obj); + QJsonObject object_album; + if (item_object.contains("album"_L1)) { + if (!item_object["album"_L1].isObject()) { + Error(u"Invalid Json reply, items album is not a object."_s, item_object); continue; } - obj_album = item_obj["album"_L1].toObject(); + object_album = item_object["album"_L1].toObject(); } else { - obj_album = item_obj; + object_album = item_object; } - if (!obj_album.contains("artist"_L1) || !obj_album.contains("image"_L1) || !obj_album.contains("title"_L1)) { - Error(u"Invalid Json reply, item is missing artist, title or image."_s, obj_album); + if (!object_album.contains("artist"_L1) || !object_album.contains("image"_L1) || !object_album.contains("title"_L1)) { + Error(u"Invalid Json reply, item is missing artist, title or image."_s, object_album); continue; } - QString album = obj_album["title"_L1].toString(); + const QString album = object_album["title"_L1].toString(); // Artist - QJsonValue value_artist = obj_album["artist"_L1]; + const QJsonValue value_artist = object_album["artist"_L1]; if (!value_artist.isObject()) { Error(u"Invalid Json reply, items (album) artist is not a object."_s, value_artist); continue; } - QJsonObject obj_artist = value_artist.toObject(); - if (!obj_artist.contains("name"_L1)) { - Error(u"Invalid Json reply, items (album) artist is missing name."_s, obj_artist); + const QJsonObject object_artist = value_artist.toObject(); + if (!object_artist.contains("name"_L1)) { + Error(u"Invalid Json reply, items (album) artist is missing name."_s, object_artist); continue; } - QString artist = obj_artist["name"_L1].toString(); + const QString artist = object_artist["name"_L1].toString(); // Image - QJsonValue value_image = obj_album["image"_L1]; - if (!value_image.isObject()) { - Error(u"Invalid Json reply, items (album) image is not a object."_s, value_image); + const QJsonValue _timer_ = object_album["image"_L1]; + if (!_timer_.isObject()) { + Error(u"Invalid Json reply, items (album) image is not a object."_s, _timer_); continue; } - QJsonObject obj_image = value_image.toObject(); - if (!obj_image.contains("large"_L1)) { - Error(u"Invalid Json reply, items (album) image is missing large."_s, obj_image); + const QJsonObject object_image = _timer_.toObject(); + if (!object_image.contains("large"_L1)) { + Error(u"Invalid Json reply, items (album) image is missing large."_s, object_image); continue; } - QUrl cover_url(obj_image["large"_L1].toString()); + const QUrl cover_url(object_image["large"_L1].toString()); CoverProviderSearchResult cover_result; cover_result.artist = artist; @@ -273,7 +279,6 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { results << cover_result; } - Q_EMIT SearchFinished(id, results); } diff --git a/src/covermanager/qobuzcoverprovider.h b/src/covermanager/qobuzcoverprovider.h index f4734f91e..f9007d93b 100644 --- a/src/covermanager/qobuzcoverprovider.h +++ b/src/covermanager/qobuzcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,44 +22,37 @@ #include "config.h" -#include -#include #include -#include #include -#include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" -#include "qobuz/qobuzservice.h" class QNetworkReply; class NetworkAccessManager; +class QobuzService; class QobuzCoverProvider : public JsonCoverProvider { Q_OBJECT public: - explicit QobuzCoverProvider(const QobuzServicePtr service, SharedPtr network, QObject *parent = nullptr); - ~QobuzCoverProvider() override; + explicit QobuzCoverProvider(const SharedPtr service, SharedPtr network, QObject *parent = nullptr); + + virtual bool authenticated() const override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; - - bool IsAuthenticated() const override { return service_ && service_->authenticated(); } - void Deauthenticate() override { if (service_) service_->Logout(); } + void ClearSession() override; private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id); private: - QByteArray GetReplyData(QNetworkReply *reply); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void Error(const QString &error, const QVariant &debug = QVariant()) override; private: - QobuzServicePtr service_; - QList replies_; + SharedPtr service_; }; #endif // QOBUZCOVERPROVIDER_H diff --git a/src/covermanager/spotifycoverprovider.cpp b/src/covermanager/spotifycoverprovider.cpp index ec3726bc0..796485e2b 100644 --- a/src/covermanager/spotifycoverprovider.cpp +++ b/src/covermanager/spotifycoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2024, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * 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,24 +19,14 @@ #include "config.h" -#include - -#include -#include -#include -#include #include #include #include -#include -#include #include -#include #include #include #include -#include -#include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" @@ -57,20 +47,33 @@ SpotifyCoverProvider::SpotifyCoverProvider(const SpotifyServicePtr service, Shar : JsonCoverProvider(u"Spotify"_s, true, true, 2.5, true, true, network, parent), service_(service) {} -SpotifyCoverProvider::~SpotifyCoverProvider() { +bool SpotifyCoverProvider::authenticated() const { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } + return service_->authenticated(); + +} + +bool SpotifyCoverProvider::use_authorization_header() const { + + return true; + +} + +QByteArray SpotifyCoverProvider::authorization_header() const { + + return service_->authorization_header(); + +} + +void SpotifyCoverProvider::ClearSession() { + + service_->ClearSession(); } bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) { - if (!IsAuthenticated()) return false; + if (!service_->authenticated()) return false; if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false; @@ -96,20 +99,7 @@ bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &alb << Param(u"type"_s, type) << Param(u"limit"_s, QString::number(kLimit)); - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QLatin1String(kApiUrl) + u"/search"_s); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - req.setRawHeader("Authorization", "Bearer " + service_->access_token().toUtf8()); - - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kApiUrl) + u"/search"_s), params); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, extract]() { HandleSearchReply(reply, id, extract); }); return true; @@ -118,50 +108,59 @@ bool SpotifyCoverProvider::StartSearch(const QString &artist, const QString &alb void SpotifyCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); } -QByteArray SpotifyCoverProvider::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult SpotifyCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("error"_L1) && json_object["error"_L1].isObject()) { + const QJsonObject object_error = json_object["error"_L1].toObject(); + if (object_error.contains("status"_L1) && object_error.contains("message"_L1)) { + const int status = object_error["status"_L1].toInt(); + const QString message = object_error["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(status); + } + } + else { + result.json_object = json_document.object(); + } } else { - data = reply->readAll(); - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - QString error; - if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("error"_L1) && json_obj["error"_L1].isObject()) { - QJsonObject obj_error = json_obj["error"_L1].toObject(); - if (obj_error.contains("status"_L1) && obj_error.contains("message"_L1)) { - int status = obj_error["status"_L1].toInt(); - QString message = obj_error["message"_L1].toString(); - error = QStringLiteral("%1 (%2)").arg(message).arg(status); - if (status == 401) Deauthenticate(); - } - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() == 204) Deauthenticate(); - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + service_->ClearSession(); + } + + return result; } @@ -172,48 +171,46 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - return; - } - - if (!json_obj.contains(extract) || !json_obj[extract].isObject()) { - Error(QStringLiteral("Json object is missing %1 object.").arg(extract), json_obj); - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - return; - } - json_obj = json_obj[extract].toObject(); - - if (!json_obj.contains("items"_L1) || !json_obj["items"_L1].isArray()) { - Error(QStringLiteral("%1 object is missing items array.").arg(extract), json_obj); - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - return; - } - - const QJsonArray array_items = json_obj["items"_L1].toArray(); - if (array_items.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); - return; - } - CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); + return; + } + + QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { + return; + } + + if (!json_object.contains(extract) || !json_object[extract].isObject()) { + Error(QStringLiteral("Json object is missing %1 object.").arg(extract), json_object); + return; + } + json_object = json_object[extract].toObject(); + + if (!json_object.contains("items"_L1) || !json_object["items"_L1].isArray()) { + Error(QStringLiteral("%1 object is missing items array.").arg(extract), json_object); + return; + } + + const QJsonArray array_items = json_object["items"_L1].toArray(); + if (array_items.isEmpty()) { + return; + } + for (const QJsonValue &value_item : array_items) { if (!value_item.isObject()) { continue; } - QJsonObject obj_item = value_item.toObject(); + const QJsonObject object_item = value_item.toObject(); - QJsonObject obj_album = obj_item; - if (obj_item.contains("album"_L1) && obj_item["album"_L1].isObject()) { - obj_album = obj_item["album"_L1].toObject(); + QJsonObject obj_album = object_item; + if (object_item.contains("album"_L1) && object_item["album"_L1].isObject()) { + obj_album = object_item["album"_L1].toObject(); } if (!obj_album.contains("artists"_L1) || !obj_album.contains("name"_L1) || !obj_album.contains("images"_L1) || !obj_album["artists"_L1].isArray() || !obj_album["images"_L1].isArray()) { @@ -221,7 +218,7 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, } const QJsonArray array_artists = obj_album["artists"_L1].toArray(); const QJsonArray array_images = obj_album["images"_L1].toArray(); - QString album = obj_album["name"_L1].toString(); + const QString album = obj_album["name"_L1].toString(); QStringList artists; for (const QJsonValue &value_artist : array_artists) { @@ -233,12 +230,12 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, for (const QJsonValue &value_image : array_images) { if (!value_image.isObject()) continue; - QJsonObject obj_image = value_image.toObject(); - if (!obj_image.contains("url"_L1) || !obj_image.contains("width"_L1) || !obj_image.contains("height"_L1)) continue; - int width = obj_image["width"_L1].toInt(); - int height = obj_image["height"_L1].toInt(); + const QJsonObject object_image = value_image.toObject(); + if (!object_image.contains("url"_L1) || !object_image.contains("width"_L1) || !object_image.contains("height"_L1)) continue; + const int width = object_image["width"_L1].toInt(); + const int height = object_image["height"_L1].toInt(); if (width < 300 || height < 300) continue; - QUrl url(obj_image["url"_L1].toString()); + const QUrl url(object_image["url"_L1].toString()); CoverProviderSearchResult result; result.album = album; result.image_url = url; @@ -248,7 +245,6 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id, } } - Q_EMIT SearchFinished(id, results); } diff --git a/src/covermanager/spotifycoverprovider.h b/src/covermanager/spotifycoverprovider.h index 48e6e1576..c20f1dd50 100644 --- a/src/covermanager/spotifycoverprovider.h +++ b/src/covermanager/spotifycoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2024, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,50 +22,41 @@ #include "config.h" -#include -#include -#include -#include #include -#include #include -#include -#include -#include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" -#include "spotify/spotifyservice.h" class QNetworkReply; class NetworkAccessManager; +class SpotifyService; + +using SpotifyServicePtr = SharedPtr; class SpotifyCoverProvider : public JsonCoverProvider { Q_OBJECT public: explicit SpotifyCoverProvider(const SpotifyServicePtr service, const SharedPtr network, QObject *parent = nullptr); - ~SpotifyCoverProvider() override; + + virtual bool authenticated() const override; + virtual bool use_authorization_header() const override; + virtual QByteArray authorization_header() const override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; - - bool IsAuthenticated() const override { return service_ && service_->authenticated(); } - void Deauthenticate() override { - if (service_) service_->Deauthenticate(); - } + void ClearSession() override; private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id, const QString &extract); private: - QByteArray GetReplyData(QNetworkReply *reply); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void Error(const QString &error, const QVariant &debug = QVariant()) override; private: - const SharedPtr service_; - QList replies_; + const SpotifyServicePtr service_; }; #endif // SPOTIFYCOVERPROVIDER_H diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp index 27f55d639..351ea077c 100644 --- a/src/covermanager/tidalcoverprovider.cpp +++ b/src/covermanager/tidalcoverprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,20 +19,17 @@ #include "config.h" -#include -#include #include #include #include #include #include -#include -#include #include #include #include #include #include +#include #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" @@ -47,20 +44,33 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr int kLimit = 10; -} +} // namespace TidalCoverProvider::TidalCoverProvider(const TidalServicePtr service, const SharedPtr network, QObject *parent) : JsonCoverProvider(u"Tidal"_s, true, true, 2.5, true, true, network, parent), service_(service) {} -TidalCoverProvider::~TidalCoverProvider() { +bool TidalCoverProvider::authenticated() const { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } + return service_->authenticated(); + +} + +bool TidalCoverProvider::use_authorization_header() const { + + return true; + +} + +QByteArray TidalCoverProvider::authorization_header() const { + + return service_->authorization_header(); + +} + +void TidalCoverProvider::ClearSession() { + + service_->ClearSession(); } @@ -89,20 +99,7 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album << Param(u"limit"_s, QString::number(kLimit)) << Param(u"countryCode"_s, service_->country_code()); - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + resource); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!service_->access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + service_->access_token().toUtf8()); - - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + resource), params); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); }); return true; @@ -111,52 +108,62 @@ bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album void TidalCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); } -QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult TidalCoverProvider::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + bool clear_session = false; + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("status"_L1) && json_object.contains("subStatus"_L1) && json_object.contains("userMessage"_L1)) { + const int status = json_object["status"_L1].toInt(); + const int sub_status = json_object["subStatus"_L1].toInt(); + const QString user_message = json_object["userMessage"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.api_error = status; + result.error_message = QStringLiteral("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + if (status == 401 && sub_status == 6001) { + clear_session = true; + } + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "status" and "userMessage" - then use that instead. - data = reply->readAll(); - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - int status = 0; - int sub_status = 0; - QString error; - if (parse_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("userMessage"_L1)) { - status = json_obj["status"_L1].toInt(); - sub_status = json_obj["subStatus"_L1].toInt(); - QString user_message = json_obj["userMessage"_L1].toString(); - error = QStringLiteral("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - if (status == 401 && sub_status == 6001) { // User does not have a valid session - service_->Logout(); - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError || clear_session) { + service_->ClearSession(); + } + + return result; } @@ -167,36 +174,34 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); + CoverProviderSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); + + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); + const QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("items"_L1)) { - Error(u"Json object is missing items."_s, json_obj); - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); + if (!json_object.contains("items"_L1)) { + Error(u"Json object is missing items."_s, json_object); return; } - QJsonValue value_items = json_obj["items"_L1]; + const QJsonValue value_items = json_object["items"_L1]; if (!value_items.isArray()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); return; } const QJsonArray array_items = value_items.toArray(); if (array_items.isEmpty()) { - Q_EMIT SearchFinished(id, CoverProviderSearchResults()); return; } - CoverProviderSearchResults results; int i = 0; for (const QJsonValue &value_item : array_items) { @@ -204,29 +209,29 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { Error(u"Invalid Json reply, items array item is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + const QJsonObject object_item = value_item.toObject(); - if (!obj_item.contains("artist"_L1)) { - Error(u"Invalid Json reply, items array item is missing artist."_s, obj_item); + if (!object_item.contains("artist"_L1)) { + Error(u"Invalid Json reply, items array item is missing artist."_s, object_item); continue; } - QJsonValue value_artist = obj_item["artist"_L1]; + const QJsonValue value_artist = object_item["artist"_L1]; if (!value_artist.isObject()) { Error(u"Invalid Json reply, items array item artist is not a object."_s, value_artist); continue; } - QJsonObject obj_artist = value_artist.toObject(); - if (!obj_artist.contains("name"_L1)) { - Error(u"Invalid Json reply, items array item artist is missing name."_s, obj_artist); + const QJsonObject object_artist = value_artist.toObject(); + if (!object_artist.contains("name"_L1)) { + Error(u"Invalid Json reply, items array item artist is missing name."_s, object_artist); continue; } - QString artist = obj_artist["name"_L1].toString(); + const QString artist = object_artist["name"_L1].toString(); - QJsonObject obj_album; - if (obj_item.contains("album"_L1)) { - QJsonValue value_album = obj_item["album"_L1]; + QJsonObject object_album; + if (object_item.contains("album"_L1)) { + QJsonValue value_album = object_item["album"_L1]; if (value_album.isObject()) { - obj_album = value_album.toObject(); + object_album = value_album.toObject(); } else { Error(u"Invalid Json reply, items array item album is not a object."_s, value_album); @@ -234,15 +239,15 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { } } else { - obj_album = obj_item; + object_album = object_item; } - if (!obj_album.contains("title"_L1) || !obj_album.contains("cover"_L1)) { - Error(u"Invalid Json reply, items array item album is missing title or cover."_s, obj_album); + if (!object_album.contains("title"_L1) || !object_album.contains("cover"_L1)) { + Error(u"Invalid Json reply, items array item album is missing title or cover."_s, object_album); continue; } - QString album = obj_album["title"_L1].toString(); - QString cover = obj_album["cover"_L1].toString().replace("-"_L1, "/"_L1); + const QString album = object_album["title"_L1].toString(); + const QString cover = object_album["cover"_L1].toString().replace("-"_L1, "/"_L1); CoverProviderSearchResult cover_result; cover_result.artist = artist; @@ -260,7 +265,6 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { } } - Q_EMIT SearchFinished(id, results); } diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h index f30893874..ef70cf72e 100644 --- a/src/covermanager/tidalcoverprovider.h +++ b/src/covermanager/tidalcoverprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,48 +22,41 @@ #include "config.h" -#include -#include -#include -#include #include -#include #include -#include -#include #include "includes/shared_ptr.h" #include "jsoncoverprovider.h" -#include "tidal/tidalservice.h" class QNetworkReply; class NetworkAccessManager; +class TidalService; + +using TidalServicePtr = SharedPtr; class TidalCoverProvider : public JsonCoverProvider { Q_OBJECT public: explicit TidalCoverProvider(const TidalServicePtr service, const SharedPtr network, QObject *parent = nullptr); - ~TidalCoverProvider() override; + + virtual bool authenticated() const override; + virtual bool use_authorization_header() const override; + virtual QByteArray authorization_header() const override; bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override; void CancelSearch(const int id) override; - - bool IsAuthenticated() const override { return service_ && service_->authenticated(); } - void Deauthenticate() override { - if (service_) service_->Logout(); - } + void ClearSession() override; private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id); private: - QByteArray GetReplyData(QNetworkReply *reply); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void Error(const QString &error, const QVariant &debug = QVariant()) override; private: const TidalServicePtr service_; - QList replies_; }; #endif // TIDALCOVERPROVIDER_H diff --git a/src/lyrics/musixmatchlyricsprovider.cpp b/src/lyrics/musixmatchlyricsprovider.cpp index fbeb8a7fb..514f068d6 100644 --- a/src/lyrics/musixmatchlyricsprovider.cpp +++ b/src/lyrics/musixmatchlyricsprovider.cpp @@ -36,17 +36,23 @@ #include #include "includes/shared_ptr.h" +#include "utilities/musixmatchprovider.h" +#include "utilities/strutils.h" #include "core/logging.h" #include "core/networkaccessmanager.h" -#include "utilities/strutils.h" #include "jsonlyricsprovider.h" #include "lyricssearchrequest.h" #include "lyricssearchresult.h" #include "musixmatchlyricsprovider.h" -#include "providers/musixmatchprovider.h" using namespace Qt::Literals::StringLiterals; using std::make_shared; +using namespace MusixmatchProvider; + +namespace { +constexpr char kApiUrl[] = "https://api.musixmatch.com/ws/1.1"; +constexpr char kApiKey[] = "Y2FhMDRlN2Y4OWE5OTIxYmZlOGMzOWQzOGI3ZGU4MjE="; +} // namespace MusixmatchLyricsProvider::MusixmatchLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"Musixmatch"_s, true, false, network, parent), use_api_(true) {} diff --git a/src/lyrics/musixmatchlyricsprovider.h b/src/lyrics/musixmatchlyricsprovider.h index a4d03fe65..8d5828e2c 100644 --- a/src/lyrics/musixmatchlyricsprovider.h +++ b/src/lyrics/musixmatchlyricsprovider.h @@ -33,12 +33,11 @@ #include "jsonlyricsprovider.h" #include "lyricssearchrequest.h" #include "lyricssearchresult.h" -#include "providers/musixmatchprovider.h" class QNetworkReply; class NetworkAccessManager; -class MusixmatchLyricsProvider : public JsonLyricsProvider, public MusixmatchProvider { +class MusixmatchLyricsProvider : public JsonLyricsProvider { Q_OBJECT public: diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp index 30ed4e81a..10ca8388d 100644 --- a/src/qobuz/qobuzbaserequest.cpp +++ b/src/qobuz/qobuzbaserequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ #include #include -#include #include #include #include @@ -46,16 +45,44 @@ using namespace Qt::Literals::StringLiterals; QobuzBaseRequest::QobuzBaseRequest(QobuzService *service, const SharedPtr network, QObject *parent) - : QObject(parent), + : JsonBaseRequest(network, parent), service_(service), network_(network) {} -QobuzBaseRequest::~QobuzBaseRequest() = default; +QString QobuzBaseRequest::service_name() const { + + return service_->name(); + +} + +bool QobuzBaseRequest::authentication_required() const { + + return true; + +} + +bool QobuzBaseRequest::authenticated() const { + + return service_->authenticated(); + +} + +bool QobuzBaseRequest::use_authorization_header() const { + + return false; + +} + +QByteArray QobuzBaseRequest::authorization_header() const { + + return QByteArray(); + +} QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided) { ParamList params = ParamList() << params_provided - << Param(u"app_id"_s, app_id()); + << Param(u"app_id"_s, service_->app_id()); std::sort(params.begin(), params.end()); @@ -64,15 +91,18 @@ QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, co url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); } - QUrl url(QString::fromLatin1(QobuzService::kApiUrl) + QLatin1Char('/') + ressource_name); + QUrl url(QString::fromLatin1(QobuzService::kApiUrl) + u'/' + ressource_name); url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - req.setRawHeader("X-App-Id", app_id().toUtf8()); - if (authenticated()) req.setRawHeader("X-User-Auth-Token", user_auth_token().toUtf8()); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + network_request.setRawHeader("X-App-Id", service_->app_id().toUtf8()); + if (authenticated()) { + network_request.setRawHeader("X-User-Auth-Token", service_->user_auth_token().toUtf8()); + } - QNetworkReply *reply = network_->get(req); + QNetworkReply *reply = network_->get(network_request); + replies_ << reply; QObject::connect(reply, &QNetworkReply::sslErrors, this, &QobuzBaseRequest::HandleSSLErrors); qLog(Debug) << "Qobuz: Sending request" << url; @@ -81,112 +111,56 @@ QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, co } -void QobuzBaseRequest::HandleSSLErrors(const QList &ssl_errors) { +JsonBaseRequest::JsonObjectResult QobuzBaseRequest::ParseJsonObject(QNetworkReply *reply) { - for (const QSslError &ssl_error : ssl_errors) { - Error(ssl_error.errorString()); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } -} - -QByteArray QobuzBaseRequest::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("code"_L1) && json_object.contains("status"_L1) && json_object.contains("message"_L1)) { + const int code = json_object["code"_L1].toInt(); + const QString message = json_object["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(code); + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "status", "code" and "message" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - if (parse_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(); - error = QStringLiteral("%1 (%2)").arg(message).arg(code); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; - -} - -QJsonObject QobuzBaseRequest::ExtractJsonObj(QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error(u"Reply from server missing Json data."_s, data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - Error(u"Received empty Json document."_s, data); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); - return QJsonObject(); - } - - return json_obj; - -} - -QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj); - -} - -QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj) { - - if (!json_obj.contains("items"_L1)) { - Error(u"Json reply is missing items."_s, json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"_L1]; - return json_items; - -} - -QString QobuzBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + u"
"_s; - } - return error_html; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + service_->ClearSession(); + + } + + return result; } diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h index 42e10218c..e880692a3 100644 --- a/src/qobuz/qobuzbaserequest.h +++ b/src/qobuz/qobuzbaserequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,31 +22,21 @@ #include "config.h" -#include -#include -#include -#include -#include -#include #include #include -#include -#include -#include #include "includes/shared_ptr.h" -#include "core/song.h" -#include "qobuzservice.h" +#include "core/jsonbaserequest.h" class QNetworkReply; class NetworkAccessManager; +class QobuzService; -class QobuzBaseRequest : public QObject { +class QobuzBaseRequest : public JsonBaseRequest { Q_OBJECT public: explicit QobuzBaseRequest(QobuzService *service, const SharedPtr network, QObject *parent = nullptr); - ~QobuzBaseRequest(); enum class Type { None, @@ -60,41 +50,16 @@ class QobuzBaseRequest : public QObject { }; protected: - using Param = QPair; - using ParamList = QList; + QString service_name() const override; + bool authentication_required() const override; + bool authenticated() const override; + bool use_authorization_header() const override; + QByteArray authorization_header() const override; QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(QByteArray &data); - QJsonValue ExtractItems(QByteArray &data); - QJsonValue ExtractItems(QJsonObject &json_obj); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - static QString ErrorsToHTML(const QStringList &errors); - - QString app_id() const { return service_->app_id(); } - QString app_secret() const { return service_->app_secret(); } - QString username() const { return service_->username(); } - QString password() const { return service_->password(); } - int format() const { return service_->format(); } - int artistssearchlimit() const { return service_->artistssearchlimit(); } - int albumssearchlimit() const { return service_->albumssearchlimit(); } - int songssearchlimit() const { return service_->songssearchlimit(); } - - qint64 user_id() const { return service_->user_id(); } - QString user_auth_token() const { return service_->user_auth_token(); } - QString device_id() const { return service_->device_id(); } - qint64 credential_id() const { return service_->credential_id(); } - - bool authenticated() const { return service_->authenticated(); } - bool login_sent() const { return service_->login_sent(); } - int max_login_attempts() const { return service_->max_login_attempts(); } - int login_attempts() const { return service_->login_attempts(); } - - private Q_SLOTS: - void HandleSSLErrors(const QList &ssl_errors); - - private: + protected: QobuzService *service_; const SharedPtr network_; }; diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp index fc905fd25..c217a580a 100644 --- a/src/qobuz/qobuzfavoriterequest.cpp +++ b/src/qobuz/qobuzfavoriterequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * 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,8 +19,6 @@ #include "config.h" -#include -#include #include #include #include @@ -39,20 +37,7 @@ using namespace Qt::Literals::StringLiterals; QobuzFavoriteRequest::QobuzFavoriteRequest(QobuzService *service, const SharedPtr network, QObject *parent) - : QobuzBaseRequest(service, network, parent), - service_(service), - network_(network) {} - -QobuzFavoriteRequest::~QobuzFavoriteRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} + : QobuzBaseRequest(service, network, parent) {} QString QobuzFavoriteRequest::FavoriteText(const FavoriteType type) { @@ -134,8 +119,8 @@ void QobuzFavoriteRequest::AddFavorites(const FavoriteType type, const SongList void QobuzFavoriteRequest::AddFavoritesRequest(const FavoriteType type, const QStringList &ids_list, const SongList &songs) { - const ParamList params = ParamList() << Param(u"app_id"_s, app_id()) - << Param(u"user_auth_token"_s, user_auth_token()) + const ParamList params = ParamList() << Param(u"app_id"_s, service_->app_id()) + << Param(u"user_auth_token"_s, service_->user_auth_token()) << Param(FavoriteMethod(type), ids_list.join(u',')); QUrlQuery url_query; @@ -159,9 +144,9 @@ void QobuzFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const Favorit return; } - GetReplyData(reply); - - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } @@ -229,8 +214,8 @@ void QobuzFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongLi void QobuzFavoriteRequest::RemoveFavoritesRequest(const FavoriteType type, const QStringList &ids_list, const SongList &songs) { - const ParamList params = ParamList() << Param(u"app_id"_s, app_id()) - << Param(u"user_auth_token"_s, user_auth_token()) + const ParamList params = ParamList() << Param(u"app_id"_s, service_->app_id()) + << Param(u"user_auth_token"_s, service_->user_auth_token()) << Param(FavoriteMethod(type), ids_list.join(u',')); QUrlQuery url_query; @@ -254,8 +239,9 @@ void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo return; } - GetReplyData(reply); - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h index 201da6fe4..1c438e90c 100644 --- a/src/qobuz/qobuzfavoriterequest.h +++ b/src/qobuz/qobuzfavoriterequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +22,6 @@ #include "config.h" -#include -#include #include #include @@ -34,13 +32,13 @@ class QNetworkReply; class QobuzService; class NetworkAccessManager; +class QobuzService; class QobuzFavoriteRequest : public QobuzBaseRequest { Q_OBJECT public: explicit QobuzFavoriteRequest(QobuzService *service, SharedPtr network, QObject *parent = nullptr); - ~QobuzFavoriteRequest(); private: enum class FavoriteType { @@ -79,10 +77,6 @@ class QobuzFavoriteRequest : public QobuzBaseRequest { void AddFavoritesRequest(const FavoriteType type, const QStringList &ids_list, const SongList &songs); void RemoveFavorites(const FavoriteType type, const SongList &songs); void RemoveFavoritesRequest(const FavoriteType type, const QStringList &ids_list, const SongList &songs); - - QobuzService *service_; - const SharedPtr network_; - QList replies_; }; #endif // QOBUZFAVORITEREQUEST_H diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp index d435cff5d..57d8e48c0 100644 --- a/src/qobuz/qobuzrequest.cpp +++ b/src/qobuz/qobuzrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,7 +21,6 @@ #include -#include #include #include #include @@ -35,6 +34,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/logging.h" @@ -62,9 +62,7 @@ constexpr int kFlushRequestsDelay = 200; QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, const SharedPtr network, const Type query_type, QObject *parent) : QobuzBaseRequest(service, network, parent), - service_(service), url_handler_(url_handler), - network_(network), timer_flush_requests_(new QTimer(this)), query_type_(query_type), query_id_(-1), @@ -105,24 +103,6 @@ QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, } -QobuzRequest::~QobuzRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - - while (!album_cover_replies_.isEmpty()) { - QNetworkReply *reply = album_cover_replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - -} - void QobuzRequest::Process() { switch (query_type_) { @@ -225,12 +205,12 @@ void QobuzRequest::FlushArtistsRequests() { while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { - Request request = artists_requests_queue_.dequeue(); + const Request request = artists_requests_queue_.dequeue(); ParamList params; if (query_type_ == Type::FavouriteArtists) { params << Param(u"type"_s, u"artists"_s); - params << Param(u"user_auth_token"_s, user_auth_token()); + params << Param(u"user_auth_token"_s, service_->user_auth_token()); } else if (query_type_ == Type::SearchArtists) params << Param(u"query"_s, search_text_); if (request.limit > 0) params << Param(u"limit"_s, QString::number(request.limit)); @@ -277,12 +257,12 @@ void QobuzRequest::FlushAlbumsRequests() { while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - Request request = albums_requests_queue_.dequeue(); + const Request request = albums_requests_queue_.dequeue(); ParamList params; if (query_type_ == Type::FavouriteAlbums) { params << Param(u"type"_s, u"albums"_s); - params << Param(u"user_auth_token"_s, user_auth_token()); + params << Param(u"user_auth_token"_s, service_->user_auth_token()); } else if (query_type_ == Type::SearchAlbums) params << Param(u"query"_s, search_text_); if (request.limit > 0) params << Param(u"limit"_s, QString::number(request.limit)); @@ -329,12 +309,12 @@ void QobuzRequest::FlushSongsRequests() { while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { - Request request = songs_requests_queue_.dequeue(); + const Request request = songs_requests_queue_.dequeue(); ParamList params; if (query_type_ == Type::FavouriteSongs) { params << Param(u"type"_s, u"tracks"_s); - params << Param(u"user_auth_token"_s, user_auth_token()); + params << Param(u"user_auth_token"_s, service_->user_auth_token()); } else if (query_type_ == Type::SearchSongs) params << Param(u"query"_s, search_text_); if (request.limit > 0) params << Param(u"limit"_s, QString::number(request.limit)); @@ -405,61 +385,59 @@ void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); --artists_requests_active_; ++artists_requests_received_; if (finished_) return; - if (data.isEmpty()) { - ArtistsFinishCheck(); + int offset = 0; + int artists_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, limit_requested, &offset, &artists_received]() { ArtistsFinishCheck(limit_requested, offset, artists_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - ArtistsFinishCheck(); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("artists"_L1)) { - ArtistsFinishCheck(); - Error(u"Json object is missing artists."_s, json_obj); + if (!json_object.contains("artists"_L1)) { + Error(u"Json object is missing artists."_s, json_object); return; } - QJsonValue value_artists = json_obj["artists"_L1]; + const QJsonValue value_artists = json_object["artists"_L1]; if (!value_artists.isObject()) { - Error(u"Json artists is not an object."_s, json_obj); - ArtistsFinishCheck(); + Error(u"Json artists is not an object."_s, json_object); return; } - QJsonObject obj_artists = value_artists.toObject(); + const QJsonObject object_artists = value_artists.toObject(); - if (!obj_artists.contains("limit"_L1) || - !obj_artists.contains("offset"_L1) || - !obj_artists.contains("total"_L1) || - !obj_artists.contains("items"_L1)) { - ArtistsFinishCheck(); - Error(u"Json artists object is missing values."_s, json_obj); + if (!object_artists.contains("limit"_L1) || + !object_artists.contains("offset"_L1) || + !object_artists.contains("total"_L1) || + !object_artists.contains("items"_L1)) { + Error(u"Json artists object is missing values."_s, json_object); return; } //int limit = obj_artists["limit"].toInt(); - int offset = obj_artists["offset"_L1].toInt(); - int artists_total = obj_artists["total"_L1].toInt(); + offset = object_artists["offset"_L1].toInt(); + int artists_total = object_artists["total"_L1].toInt(); if (offset_requested == 0) { artists_total_ = artists_total; } else if (artists_total != artists_total_) { Error(QStringLiteral("total returned does not match previous total! %1 != %2").arg(artists_total).arg(artists_total_)); - ArtistsFinishCheck(); return; } if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - ArtistsFinishCheck(); return; } @@ -467,20 +445,18 @@ void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re Q_EMIT UpdateProgress(query_id_, GetProgress(artists_received_, artists_total_)); } - QJsonValue value_items = ExtractItems(obj_artists); - if (!value_items.isArray()) { - ArtistsFinishCheck(); + const JsonArrayResult json_array_result = GetJsonArray(object_artists, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { // Empty array means no results if (offset_requested == 0) no_results_ = true; - ArtistsFinishCheck(); return; } - int artists_received = 0; for (const QJsonValue &value_item : array_items) { ++artists_received; @@ -525,8 +501,6 @@ void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re if (offset_requested != 0) Q_EMIT UpdateProgress(query_id_, GetProgress(artists_received_, artists_total_)); - ArtistsFinishCheck(limit_requested, offset, artists_received); - } void QobuzRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { @@ -619,86 +593,84 @@ void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - AlbumsFinishCheck(artist_requested); + int offset = 0; + int albums_total = 0; + int albums_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, artist_requested, limit_requested, &offset, &albums_total, &albums_received]() { AlbumsFinishCheck(artist_requested, limit_requested, offset, albums_total, albums_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(artist_requested); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } Artist artist = artist_requested; - if (json_obj.contains("id"_L1) && json_obj.contains("name"_L1)) { - if (json_obj["id"_L1].isString()) { - artist.artist_id = json_obj["id"_L1].toString(); + if (json_object.contains("id"_L1) && json_object.contains("name"_L1)) { + if (json_object["id"_L1].isString()) { + artist.artist_id = json_object["id"_L1].toString(); } else { - artist.artist_id = QString::number(json_obj["id"_L1].toInt()); + artist.artist_id = QString::number(json_object["id"_L1].toInt()); } - artist.artist = json_obj["name"_L1].toString(); + artist.artist = json_object["name"_L1].toString(); } if (artist.artist_id != artist_requested.artist_id) { - AlbumsFinishCheck(artist_requested); - Error(u"Artist ID returned does not match artist ID requested."_s, json_obj); + Error(u"Artist ID returned does not match artist ID requested."_s, json_object); return; } - if (!json_obj.contains("albums"_L1)) { - AlbumsFinishCheck(artist_requested); - Error(u"Json object is missing albums."_s, json_obj); + if (!json_object.contains("albums"_L1)) { + Error(u"Json object is missing albums."_s, json_object); return; } - QJsonValue value_albums = json_obj["albums"_L1]; + const QJsonValue value_albums = json_object["albums"_L1]; if (!value_albums.isObject()) { - Error(u"Json albums is not an object."_s, json_obj); - AlbumsFinishCheck(artist_requested); + Error(u"Json albums is not an object."_s, json_object); return; } - QJsonObject obj_albums = value_albums.toObject(); + const QJsonObject object_albums = value_albums.toObject(); - if (!obj_albums.contains("limit"_L1) || - !obj_albums.contains("offset"_L1) || - !obj_albums.contains("total"_L1) || - !obj_albums.contains("items"_L1)) { - AlbumsFinishCheck(artist_requested); - Error(u"Json albums object is missing values."_s, json_obj); + if (!object_albums.contains("limit"_L1) || + !object_albums.contains("offset"_L1) || + !object_albums.contains("total"_L1) || + !object_albums.contains("items"_L1)) { + Error(u"Json albums object is missing values."_s, json_object); return; } //int limit = obj_albums["limit"].toInt(); - int offset = obj_albums["offset"_L1].toInt(); - int albums_total = obj_albums["total"_L1].toInt(); + offset = object_albums["offset"_L1].toInt(); + albums_total = object_albums["total"_L1].toInt(); if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - AlbumsFinishCheck(artist_requested); return; } - QJsonValue value_items = ExtractItems(obj_albums); - if (!value_items.isArray()) { - AlbumsFinishCheck(artist_requested); + const JsonArrayResult json_array_result = GetJsonArray(object_albums, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { if ((query_type_ == Type::FavouriteAlbums || query_type_ == Type::SearchAlbums) && offset_requested == 0) { no_results_ = true; } - AlbumsFinishCheck(artist_requested); return; } - int albums_received = 0; for (const QJsonValue &value_item : array_items) { ++albums_received; @@ -762,8 +734,6 @@ void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req Q_EMIT UpdateProgress(query_id_, GetProgress(albums_received_, albums_total_)); } - AlbumsFinishCheck(artist_requested, limit_requested, offset, albums_total, albums_received); - } void QobuzRequest::AlbumsFinishCheck(const Artist &artist, const int limit, const int offset, const int albums_total, const int albums_received) { @@ -845,7 +815,7 @@ void QobuzRequest::FlushAlbumSongsRequests() { while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { - AlbumSongsRequest request = album_songs_requests_queue_.dequeue(); + const AlbumSongsRequest request = album_songs_requests_queue_.dequeue(); ParamList params = ParamList() << Param(u"album_id"_s, request.album.album_id); if (request.offset > 0) params << Param(u"offset"_s, QString::number(request.offset)); QNetworkReply *reply = CreateRequest(u"album/get"_s, params); @@ -876,51 +846,53 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); + Artist album_artist; + Album album; + int songs_total = 0; + int songs_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, &album_artist, &album, limit_requested, offset_requested, &songs_total, &songs_received]() { SongsFinishCheck(album_artist, album, limit_requested, offset_requested, songs_total, songs_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("tracks"_L1)) { - Error(u"Json object is missing tracks."_s, json_obj); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); + if (!json_object.contains("tracks"_L1)) { + Error(u"Json object is missing tracks."_s, json_object); return; } - Artist album_artist = artist_requested; - Album album = album_requested; + album_artist = artist_requested; + album = album_requested; - if (json_obj.contains("id"_L1) && json_obj.contains("title"_L1)) { - if (json_obj["id"_L1].isString()) { - album.album_id = json_obj["id"_L1].toString(); + if (json_object.contains("id"_L1) && json_object.contains("title"_L1)) { + if (json_object["id"_L1].isString()) { + album.album_id = json_object["id"_L1].toString(); } else { - album.album_id = QString::number(json_obj["id"_L1].toInt()); + album.album_id = QString::number(json_object["id"_L1].toInt()); } - album.album = json_obj["title"_L1].toString(); + album.album = json_object["title"_L1].toString(); } - if (json_obj.contains("artist"_L1)) { - QJsonValue value_artist = json_obj["artist"_L1]; + if (json_object.contains("artist"_L1)) { + QJsonValue value_artist = json_object["artist"_L1]; if (!value_artist.isObject()) { Error(u"Invalid Json reply, album artist is not a object."_s, value_artist); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); return; } QJsonObject obj_artist = value_artist.toObject(); if (!obj_artist.contains("id"_L1) || !obj_artist.contains("name"_L1)) { Error(u"Invalid Json reply, album artist is missing id or name."_s, obj_artist); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); return; } if (obj_artist["id"_L1].isString()) { @@ -932,17 +904,15 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ album_artist.artist = obj_artist["name"_L1].toString(); } - if (json_obj.contains("image"_L1)) { - QJsonValue value_image = json_obj["image"_L1]; + if (json_object.contains("image"_L1)) { + QJsonValue value_image = json_object["image"_L1]; if (!value_image.isObject()) { Error(u"Invalid Json reply, album image is not a object."_s, value_image); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); return; } QJsonObject obj_image = value_image.toObject(); if (!obj_image.contains("large"_L1)) { Error(u"Invalid Json reply, album image is missing large."_s, obj_image); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); return; } QString album_image = obj_image["large"_L1].toString(); @@ -951,10 +921,9 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ } } - QJsonValue value_tracks = json_obj["tracks"_L1]; + QJsonValue value_tracks = json_object["tracks"_L1]; if (!value_tracks.isObject()) { - Error(u"Json tracks is not an object."_s, json_obj); - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); + Error(u"Json tracks is not an object."_s, json_object); return; } QJsonObject obj_tracks = value_tracks.toObject(); @@ -963,51 +932,47 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ !obj_tracks.contains("offset"_L1) || !obj_tracks.contains("total"_L1) || !obj_tracks.contains("items"_L1)) { - SongsFinishCheck(artist_requested, album_requested, limit_requested, offset_requested); - Error(u"Json songs object is missing values."_s, json_obj); + Error(u"Json songs object is missing values."_s, json_object); return; } //int limit = obj_tracks["limit"].toInt(); - int offset = obj_tracks["offset"_L1].toInt(); - int songs_total = obj_tracks["total"_L1].toInt(); + const int offset = obj_tracks["offset"_L1].toInt(); + songs_total = obj_tracks["total"_L1].toInt(); if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - SongsFinishCheck(album_artist, album, limit_requested, offset_requested, songs_total); return; } - QJsonValue value_items = ExtractItems(obj_tracks); - if (!value_items.isArray()) { - SongsFinishCheck(album_artist, album, limit_requested, offset_requested, songs_total); + const JsonArrayResult json_array_result = GetJsonArray(obj_tracks, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { if ((query_type_ == Type::FavouriteSongs || query_type_ == Type::SearchSongs) && offset_requested == 0) { no_results_ = true; } - SongsFinishCheck(album_artist, album, limit_requested, offset_requested, songs_total); return; } bool compilation = false; bool multidisc = false; SongList songs; - int songs_received = 0; for (const QJsonValue &value_item : array_items) { if (!value_item.isObject()) { Error(u"Invalid Json reply, track is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + const QJsonObject object_item = value_item.toObject(); ++songs_received; Song song(Song::Source::Qobuz); - ParseSong(song, obj_item, album_artist, album); + ParseSong(song, object_item, album_artist, album); if (!song.is_valid()) continue; if (song.disc() >= 2) multidisc = true; if (song.is_compilation()) compilation = true; @@ -1025,8 +990,6 @@ void QobuzRequest::SongsReceived(QNetworkReply *reply, const Artist &artist_requ Q_EMIT UpdateProgress(query_id_, GetProgress(songs_received_, songs_total_)); } - SongsFinishCheck(album_artist, album, limit_requested, offset_requested, songs_total, songs_received); - } void QobuzRequest::SongsFinishCheck(const Artist &artist, const Album &album, const int limit, const int offset, const int songs_total, const int songs_received) { @@ -1291,25 +1254,18 @@ void QobuzRequest::AddAlbumCoverRequest(const Song &song) { void QobuzRequest::FlushAlbumCoverRequests() { while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { - - AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); - - QNetworkRequest req(request.url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - album_cover_replies_ << reply; + const AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); + QNetworkReply *reply = CreateGetRequest(request.url); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumCoverReceived(reply, request.url, request.filename); }); - ++album_covers_requests_active_; - } } void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename) { - if (album_cover_replies_.contains(reply)) { - album_cover_replies_.removeAll(reply); + if (replies_.contains(reply)) { + replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); } @@ -1323,24 +1279,23 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur if (finished_) return; + const QScopeGuard finish_check = qScopeGuard([this]() { AlbumCoverFinishCheck(); }); + Q_EMIT UpdateProgress(query_id_, GetProgress(album_covers_requests_received_, album_covers_requests_total_)); if (!album_covers_requests_sent_.contains(cover_url)) { - AlbumCoverFinishCheck(); return; } if (reply->error() != QNetworkReply::NoError) { Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); return; } if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { Error(QStringLiteral("Received HTTP code %1 for %2.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()).arg(cover_url.toString())); if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); return; } @@ -1351,7 +1306,6 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) { Error(QStringLiteral("Unsupported mimetype for image reader %1 for %2").arg(mimetype, cover_url.toString())); if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); return; } @@ -1359,7 +1313,6 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur if (data.isEmpty()) { Error(QStringLiteral("Received empty image data for %1").arg(cover_url.toString())); if (album_covers_requests_sent_.contains(cover_url)) album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); return; } @@ -1389,8 +1342,6 @@ void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_ur Error(QStringLiteral("Error decoding image data from %1").arg(cover_url.toString())); } - AlbumCoverFinishCheck(); - } void QobuzRequest::AlbumCoverFinishCheck() { @@ -1424,16 +1375,20 @@ void QobuzRequest::FinishCheck() { } finished_ = true; if (no_results_ && songs_.isEmpty()) { - if (IsSearch()) + if (IsSearch()) { Q_EMIT Results(query_id_, SongMap(), tr("No match.")); - else + } + else { Q_EMIT Results(query_id_, SongMap(), QString()); + } } else { - if (songs_.isEmpty() && errors_.isEmpty()) + if (songs_.isEmpty() && error_.isEmpty()) { Q_EMIT Results(query_id_, songs_, tr("Unknown error")); - else - Q_EMIT Results(query_id_, songs_, ErrorsToHTML(errors_)); + } + else { + Q_EMIT Results(query_id_, songs_, error_); + } } } @@ -1445,20 +1400,22 @@ int QobuzRequest::GetProgress(const int count, const int total) { } -void QobuzRequest::Error(const QString &error, const QVariant &debug) { +void QobuzRequest::Error(const QString &error_message, const QVariant &debug_output) { - if (!error.isEmpty()) { - errors_ << error; - qLog(Error) << "Qobuz:" << error; + qLog(Error) << "Qobuz:" << error_message; + if (debug_output.isValid()) { + qLog(Debug) << debug_output; } - if (debug.isValid()) qLog(Debug) << debug; - FinishCheck(); + + error_ = QStringLiteral("Qobuz: %1").arg(error_message); } -void QobuzRequest::Warn(const QString &error, const QVariant &debug) { +void QobuzRequest::Warn(const QString &error_message, const QVariant &debug_output) { - qLog(Error) << "Qobuz:" << error; - if (debug.isValid()) qLog(Debug) << debug; + qLog(Error) << "Qobuz:" << error_message; + if (debug_output.isValid()) { + qLog(Debug) << debug_output; + } } diff --git a/src/qobuz/qobuzrequest.h b/src/qobuz/qobuzrequest.h index b98cae1bf..c7dd09a8c 100644 --- a/src/qobuz/qobuzrequest.h +++ b/src/qobuz/qobuzrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,11 +22,6 @@ #include "config.h" -#include -#include -#include -#include -#include #include #include #include @@ -36,6 +31,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/song.h" @@ -51,9 +47,7 @@ class QobuzRequest : public QobuzBaseRequest { Q_OBJECT public: - explicit QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, const SharedPtr network, const Type query_type, QObject *parent = nullptr); - ~QobuzRequest() override; void ReloadSettings(); @@ -98,8 +92,6 @@ class QobuzRequest : public QobuzBaseRequest { }; Q_SIGNALS: - void LoginSuccess(); - void LoginFailure(const QString &failure_reason); void Results(const int id, const SongMap &songs, const QString &error); void UpdateStatus(const int id, const QString &text); void UpdateProgress(const int id, const int max); @@ -119,7 +111,6 @@ class QobuzRequest : public QobuzBaseRequest { void AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename); private: - bool IsQuery() const { return (query_type_ == Type::FavouriteArtists || query_type_ == Type::FavouriteAlbums || query_type_ == Type::FavouriteSongs); } bool IsSearch() const { return (query_type_ == Type::SearchArtists || query_type_ == Type::SearchAlbums || query_type_ == Type::SearchSongs); } @@ -144,9 +135,9 @@ class QobuzRequest : public QobuzBaseRequest { void AddSongsSearchRequest(const int offset = 0); void FlushSongsRequests(); - void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0); - void AlbumsFinishCheck(const Artist &artist, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0); - void SongsFinishCheck(const Artist &artist, const Album &album, const int limit = 0, const int offset = 0, const int songs_total = 0, const int songs_received = 0); + void ArtistsFinishCheck(const int limit, const int offset, const int artists_received); + void AlbumsFinishCheck(const Artist &artist, const int limit, const int offset, const int albums_total, const int albums_received); + void SongsFinishCheck(const Artist &artist, const Album &album, const int limit, const int offset, const int songs_total, const int songs_received); void AddArtistAlbumsRequest(const Artist &artist, const int offset = 0); void FlushArtistAlbumsRequests(); @@ -167,12 +158,10 @@ class QobuzRequest : public QobuzBaseRequest { int GetProgress(const int count, const int total); void FinishCheck(); - static void Warn(const QString &error, const QVariant &debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; + static void Warn(const QString &error_message, const QVariant &debug_output = QVariant()); + void Error(const QString &error_message, const QVariant &debug_output = QVariant()); - QobuzService *service_; QobuzUrlHandler *url_handler_; - const SharedPtr network_; QTimer *timer_flush_requests_; const Type query_type_; @@ -228,10 +217,10 @@ class QobuzRequest : public QobuzBaseRequest { int album_covers_requests_received_; SongMap songs_; - QStringList errors_; bool no_results_; - QList replies_; - QList album_cover_replies_; + QString error_; }; +using QobuzRequestPtr = QScopedPointer; + #endif // QOBUZREQUEST_H diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp index 786b8e210..aa59f2d3c 100644 --- a/src/qobuz/qobuzservice.cpp +++ b/src/qobuz/qobuzservice.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * 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 #include -#include #include #include #include @@ -37,6 +35,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/logging.h" @@ -173,7 +172,7 @@ QobuzService::~QobuzService() { } while (!stream_url_requests_.isEmpty()) { - SharedPtr stream_url_req = stream_url_requests_.take(stream_url_requests_.firstKey()); + QSharedPointer 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 &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 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"
"_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); } diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h index 638517008..75cae0623 100644 --- a/src/qobuz/qobuzservice.h +++ b/src/qobuz/qobuzservice.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +22,6 @@ #include "config.h" -#include - #include #include #include @@ -36,6 +34,8 @@ #include #include #include +#include +#include #include "includes/shared_ptr.h" #include "core/song.h" @@ -57,6 +57,8 @@ class CollectionBackend; class CollectionModel; class CollectionFilter; +using QobuzRequestPtr = QScopedPointer; + class QobuzService : public StreamingService { Q_OBJECT @@ -77,7 +79,7 @@ class QobuzService : public StreamingService { void Exit() override; void ReloadSettings() override; - void Logout(); + void ClearSession(); int Search(const QString &text, const SearchType type) override; void CancelSearch() override; @@ -169,10 +171,10 @@ class QobuzService : public StreamingService { QTimer *timer_search_delay_; QTimer *timer_login_attempt_; - SharedPtr artists_request_; - SharedPtr albums_request_; - SharedPtr songs_request_; - SharedPtr search_request_; + QobuzRequestPtr artists_request_; + QobuzRequestPtr albums_request_; + QobuzRequestPtr songs_request_; + QobuzRequestPtr search_request_; QobuzFavoriteRequest *favorite_request_; QString app_id_; @@ -202,9 +204,7 @@ class QobuzService : public StreamingService { int login_attempts_; uint next_stream_url_request_id_; - QMap> stream_url_requests_; - - QStringList login_errors_; + QMap> stream_url_requests_; QList wait_for_exit_; QList replies_; diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp index a435117d1..3c444015e 100644 --- a/src/qobuz/qobuzstreamurlrequest.cpp +++ b/src/qobuz/qobuzstreamurlrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,14 +22,11 @@ #include #include -#include -#include -#include #include #include -#include #include #include +#include #include #include #include @@ -47,7 +44,6 @@ using namespace Qt::Literals::StringLiterals; QobuzStreamURLRequest::QobuzStreamURLRequest(QobuzService *service, const SharedPtr network, const QUrl &media_url, const uint id, QObject *parent) : QobuzBaseRequest(service, network, parent), - service_(service), reply_(nullptr), media_url_(media_url), id_(id), @@ -81,7 +77,7 @@ void QobuzStreamURLRequest::LoginComplete(const bool success, const QString &err void QobuzStreamURLRequest::Process() { - if (app_id().isEmpty() || app_secret().isEmpty()) { + if (service_->app_id().isEmpty() || service_->app_secret().isEmpty()) { Q_EMIT StreamURLFailure(id_, media_url_, tr("Missing Qobuz app ID or secret.")); return; } @@ -116,9 +112,9 @@ void QobuzStreamURLRequest::GetStreamURL() { reply_->deleteLater(); } - quint64 timestamp = QDateTime::currentSecsSinceEpoch(); + const quint64 timestamp = QDateTime::currentSecsSinceEpoch(); - ParamList params_to_sign = ParamList() << Param(u"format_id"_s, QString::number(format())) + ParamList params_to_sign = ParamList() << Param(u"format_id"_s, QString::number(service_->format())) << Param(u"track_id"_s, QString::number(song_id_)); std::sort(params_to_sign.begin(), params_to_sign.end()); @@ -129,7 +125,7 @@ void QobuzStreamURLRequest::GetStreamURL() { data_to_sign += param.first + param.second; } data_to_sign += QString::number(timestamp); - data_to_sign += app_secret(); + data_to_sign += service_->app_secret(); QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5); const QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, u'0').toLower(); @@ -137,7 +133,7 @@ void QobuzStreamURLRequest::GetStreamURL() { ParamList params = params_to_sign; params << Param(u"request_ts"_s, QString::number(timestamp)); params << Param(u"request_sig"_s, signature); - params << Param(u"user_auth_token"_s, user_auth_token()); + params << Param(u"user_auth_token"_s, service_->user_auth_token()); std::sort(params.begin(), params.end()); @@ -150,48 +146,49 @@ void QobuzStreamURLRequest::StreamURLReceived() { if (!reply_) return; - QByteArray data = GetReplyData(reply_); + Q_ASSERT(replies_.contains(reply_)); + replies_.removeAll(reply_); + + const JsonObjectResult json_object_result = ParseJsonObject(reply_); QObject::disconnect(reply_, nullptr, this, nullptr); reply_->deleteLater(); reply_ = nullptr; - if (data.isEmpty()) { - if (!authenticated() && login_sent() && tries_ <= 1) { + if (!json_object_result.success()) { + if (!authenticated() && service_->login_sent() && tries_ <= 1) { need_login_ = true; return; } - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + const QJsonObject &json_object = json_object_result.json_object; + + if (json_object.isEmpty()) { + Q_EMIT StreamURLFailure(id_, media_url_, u"Empty json object."_s); return; } - if (!json_obj.contains("track_id"_L1)) { - Error(u"Invalid Json reply, stream url is missing track_id."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + if (!json_object.contains("track_id"_L1)) { + Q_EMIT StreamURLFailure(id_, media_url_, u"Invalid Json reply, stream url is missing track_id."_s); return; } - int track_id = json_obj["track_id"_L1].toInt(); + const int track_id = json_object["track_id"_L1].toInt(); if (track_id != song_id_) { - Error(u"Incorrect track ID returned."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, u"Incorrect track ID returned."_s); return; } - if (!json_obj.contains("mime_type"_L1) || !json_obj.contains("url"_L1)) { - Error(u"Invalid Json reply, stream url is missing url or mime_type."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + if (!json_object.contains("mime_type"_L1) || !json_object.contains("url"_L1)) { + Q_EMIT StreamURLFailure(id_, media_url_, u"Invalid Json reply, stream url is missing url or mime_type."_s); return; } - QUrl url(json_obj["url"_L1].toString()); - QString mimetype = json_obj["mime_type"_L1].toString(); + const QUrl url(json_object["url"_L1].toString()); + const QString mimetype = json_object["mime_type"_L1].toString(); Song::FileType filetype(Song::FileType::Unknown); QMimeDatabase mimedb; @@ -206,34 +203,23 @@ void QobuzStreamURLRequest::StreamURLReceived() { } if (!url.isValid()) { - Error(u"Returned stream url is invalid."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, u"Returned stream url is invalid."_s); return; } qint64 duration = -1; - if (json_obj.contains("duration"_L1)) { - duration = json_obj["duration"_L1].toInt() * kNsecPerSec; + if (json_object.contains("duration"_L1)) { + duration = json_object["duration"_L1].toInt() * kNsecPerSec; } int samplerate = -1; - if (json_obj.contains("sampling_rate"_L1)) { - samplerate = static_cast(json_obj["sampling_rate"_L1].toDouble()) * 1000; + if (json_object.contains("sampling_rate"_L1)) { + samplerate = static_cast(json_object["sampling_rate"_L1].toDouble()) * 1000; } int bit_depth = -1; - if (json_obj.contains("bit_depth"_L1)) { - bit_depth = static_cast(json_obj["bit_depth"_L1].toDouble()); + if (json_object.contains("bit_depth"_L1)) { + bit_depth = static_cast(json_object["bit_depth"_L1].toDouble()); } Q_EMIT StreamURLSuccess(id_, media_url_, url, filetype, samplerate, bit_depth, duration); } - -void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) { - qLog(Error) << "Qobuz:" << error; - errors_ << error; - } - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h index ad3f2ce2c..9a5282e41 100644 --- a/src/qobuz/qobuzstreamurlrequest.h +++ b/src/qobuz/qobuzstreamurlrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,12 +22,10 @@ #include "config.h" -#include -#include #include #include -#include #include +#include #include "includes/shared_ptr.h" #include "core/song.h" @@ -65,16 +63,14 @@ class QobuzStreamURLRequest : public QobuzBaseRequest { void LoginComplete(const bool success, const QString &error = QString()); private: - void Error(const QString &error, const QVariant &debug = QVariant()); - - QobuzService *service_; QNetworkReply *reply_; QUrl media_url_; uint id_; int song_id_; int tries_; bool need_login_; - QStringList errors_; }; +using QobuzStreamURLRequestPtr = QSharedPointer; + #endif // QOBUZSTREAMURLREQUEST_H diff --git a/src/qobuz/qobuzurlhandler.cpp b/src/qobuz/qobuzurlhandler.cpp index 6f4657a40..12874d029 100644 --- a/src/qobuz/qobuzurlhandler.cpp +++ b/src/qobuz/qobuzurlhandler.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,7 +19,6 @@ #include "config.h" -#include #include #include diff --git a/src/qobuz/qobuzurlhandler.h b/src/qobuz/qobuzurlhandler.h index 3d6c428f9..cd6f54082 100644 --- a/src/qobuz/qobuzurlhandler.h +++ b/src/qobuz/qobuzurlhandler.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,8 +20,6 @@ #ifndef QOBUZURLHANDLER_H #define QOBUZURLHANDLER_H -#include -#include #include #include #include diff --git a/src/settings/coverssettingspage.cpp b/src/settings/coverssettingspage.cpp index 970eeec16..253b63ffc 100644 --- a/src/settings/coverssettingspage.cpp +++ b/src/settings/coverssettingspage.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -89,6 +89,14 @@ CoversSettingsPage::CoversSettingsPage(SettingsDialog *dialog, const SharedPtrproviders->currentItem(), nullptr); + + SettingsPage::showEvent(e); + +} + void CoversSettingsPage::Load() { ui_->providers->clear(); @@ -206,7 +214,7 @@ void CoversSettingsPage::ProvidersCurrentItemChanged(QListWidgetItem *item_curre if (item_previous) { CoverProvider *provider = cover_providers_->ProviderByName(item_previous->text()); - if (provider && provider->AuthenticationRequired()) DisconnectAuthentication(provider); + if (provider && provider->authentication_required()) DisconnectAuthentication(provider); } if (item_current) { @@ -215,21 +223,21 @@ void CoversSettingsPage::ProvidersCurrentItemChanged(QListWidgetItem *item_curre ui_->providers_down->setEnabled(row != ui_->providers->count() - 1); CoverProvider *provider = cover_providers_->ProviderByName(item_current->text()); if (provider) { - if (provider->AuthenticationRequired()) { - if (provider->name() == "Tidal"_L1 && !provider->IsAuthenticated()) { + if (provider->authentication_required()) { + if (provider->name() == "Tidal"_L1 && !provider->authenticated()) { DisableAuthentication(); ui_->label_auth_info->setText(tr("Use Tidal settings to authenticate.")); } - else if (provider->name() == "Spotify"_L1 && !provider->IsAuthenticated()) { + else if (provider->name() == "Spotify"_L1 && !provider->authenticated()) { DisableAuthentication(); ui_->label_auth_info->setText(tr("Use Spotify settings to authenticate.")); } - else if (provider->name() == "Qobuz"_L1 && !provider->IsAuthenticated()) { + else if (provider->name() == "Qobuz"_L1 && !provider->authenticated()) { DisableAuthentication(); ui_->label_auth_info->setText(tr("Use Qobuz settings to authenticate.")); } else { - ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); + ui_->login_state->SetLoggedIn(provider->authenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); ui_->button_authenticate->setEnabled(true); ui_->button_authenticate->show(); ui_->login_state->show(); @@ -331,7 +339,7 @@ void CoversSettingsPage::LogoutClicked() { if (!ui_->providers->currentItem()) return; CoverProvider *provider = cover_providers_->ProviderByName(ui_->providers->currentItem()->text()); if (!provider) return; - provider->Deauthenticate(); + provider->ClearSession(); if (provider->name() == "Tidal"_L1) { DisableAuthentication(); @@ -365,7 +373,7 @@ void CoversSettingsPage::AuthenticationSuccess() { } -void CoversSettingsPage::AuthenticationFailure(const QStringList &errors) { +void CoversSettingsPage::AuthenticationFailure(const QString &error) { CoverProvider *provider = qobject_cast(sender()); if (!provider) return; @@ -373,7 +381,7 @@ void CoversSettingsPage::AuthenticationFailure(const QStringList &errors) { if (!isVisible() || !ui_->providers->currentItem() || ui_->providers->currentItem()->text() != provider->name()) return; - QMessageBox::warning(this, tr("Authentication failed"), errors.join(u'\n')); + QMessageBox::warning(this, tr("Authentication failed"), error); ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut); ui_->button_authenticate->setEnabled(true); diff --git a/src/settings/coverssettingspage.h b/src/settings/coverssettingspage.h index 33a46e629..4ae74f1f5 100644 --- a/src/settings/coverssettingspage.h +++ b/src/settings/coverssettingspage.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2021, Jonas Kvinge + * Copyright 2020-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,6 +30,7 @@ #include "settings/settingspage.h" class QListWidgetItem; +class QShowEvent; class CoverProviders; class CoverProvider; @@ -46,6 +47,9 @@ class CoversSettingsPage : public SettingsPage { void Load() override; void Save() override; + protected: + void showEvent(QShowEvent *e) override; + private: void NoProviderSelected(); void ProvidersMove(const int d); @@ -65,7 +69,7 @@ class CoversSettingsPage : public SettingsPage { void AuthenticateClicked(); void LogoutClicked(); void AuthenticationSuccess(); - void AuthenticationFailure(const QStringList &errors); + void AuthenticationFailure(const QString &error); void CoverSaveInAlbumDirChanged(); void TypesCurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous); void TypesItemSelectionChanged(); diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp index 9e0ea81e3..36ddf283c 100644 --- a/src/settings/qobuzsettingspage.cpp +++ b/src/settings/qobuzsettingspage.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,6 +70,13 @@ QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *dialog, const SharedPtrlogin_state->SetLoggedIn(service_->authenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); + SettingsPage::showEvent(e); + +} + void QobuzSettingsPage::Load() { Settings s; @@ -157,7 +164,7 @@ bool QobuzSettingsPage::eventFilter(QObject *object, QEvent *event) { void QobuzSettingsPage::LogoutClicked() { - service_->Logout(); + service_->ClearSession(); ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut); ui_->button_login->setEnabled(true); diff --git a/src/settings/qobuzsettingspage.h b/src/settings/qobuzsettingspage.h index df276e72b..6576e9b4c 100644 --- a/src/settings/qobuzsettingspage.h +++ b/src/settings/qobuzsettingspage.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ #include "includes/shared_ptr.h" #include "settings/settingspage.h" +class QShowEvent; class QEvent; class SettingsDialog; class QobuzService; @@ -43,6 +44,9 @@ class QobuzSettingsPage : public SettingsPage { bool eventFilter(QObject *object, QEvent *event) override; + protected: + void showEvent(QShowEvent *e) override; + Q_SIGNALS: void Login(const QString &username, const QString &password, const QString &token); diff --git a/src/settings/spotifysettingspage.cpp b/src/settings/spotifysettingspage.cpp index 4f125bfae..a21f93c2a 100644 --- a/src/settings/spotifysettingspage.cpp +++ b/src/settings/spotifysettingspage.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -81,6 +81,13 @@ SpotifySettingsPage::SpotifySettingsPage(SettingsDialog *dialog, const SharedPtr SpotifySettingsPage::~SpotifySettingsPage() { delete ui_; } +void SpotifySettingsPage::showEvent(QShowEvent *e) { + + ui_->login_state->SetLoggedIn(service_->authenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); + SettingsPage::showEvent(e); + +} + void SpotifySettingsPage::Load() { Settings s; @@ -139,7 +146,7 @@ bool SpotifySettingsPage::eventFilter(QObject *object, QEvent *event) { void SpotifySettingsPage::LogoutClicked() { - service_->Deauthenticate(); + service_->ClearSession(); ui_->button_login->setEnabled(true); ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut); diff --git a/src/settings/spotifysettingspage.h b/src/settings/spotifysettingspage.h index f5eae9f7f..991027603 100644 --- a/src/settings/spotifysettingspage.h +++ b/src/settings/spotifysettingspage.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,7 @@ #include "settings/settingspage.h" class QEvent; +class QShowEvent; class SpotifyService; class SettingsDialog; class Ui_SpotifySettingsPage; @@ -45,6 +46,9 @@ class SpotifySettingsPage : public SettingsPage { bool eventFilter(QObject *object, QEvent *event) override; + protected: + void showEvent(QShowEvent *e) override; + Q_SIGNALS: void Authorize(); diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp index 70dd24335..02bca1f45 100644 --- a/src/settings/tidalsettingspage.cpp +++ b/src/settings/tidalsettingspage.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -81,6 +81,13 @@ TidalSettingsPage::TidalSettingsPage(SettingsDialog *dialog, SharedPtrlogin_state->SetLoggedIn(service_->authenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); + SettingsPage::showEvent(e); + +} + void TidalSettingsPage::Load() { Settings s; @@ -164,7 +171,7 @@ bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) { void TidalSettingsPage::LogoutClicked() { - service_->Logout(); + service_->ClearSession(); ui_->button_login->setEnabled(true); ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut); diff --git a/src/settings/tidalsettingspage.h b/src/settings/tidalsettingspage.h index 03c435e7f..32371bc78 100644 --- a/src/settings/tidalsettingspage.h +++ b/src/settings/tidalsettingspage.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ #include "includes/shared_ptr.h" #include "settings/settingspage.h" +class QShowEvent; class QEvent; class TidalService; class SettingsDialog; @@ -45,6 +46,9 @@ class TidalSettingsPage : public SettingsPage { bool eventFilter(QObject *object, QEvent *event) override; + protected: + void showEvent(QShowEvent *e) override; + Q_SIGNALS: void Authorize(const QString &client_id); diff --git a/src/spotify/spotifybaserequest.cpp b/src/spotify/spotifybaserequest.cpp index 1475dcc4d..68b703cd0 100644 --- a/src/spotify/spotifybaserequest.cpp +++ b/src/spotify/spotifybaserequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * 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,23 +19,10 @@ #include "config.h" -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#include -#include -#include #include "includes/shared_ptr.h" -#include "core/logging.h" #include "core/networkaccessmanager.h" #include "spotifyservice.h" #include "spotifybaserequest.h" @@ -43,143 +30,97 @@ using namespace Qt::Literals::StringLiterals; SpotifyBaseRequest::SpotifyBaseRequest(SpotifyService *service, const SharedPtr network, QObject *parent) - : QObject(parent), - service_(service), - network_(network) {} + : JsonBaseRequest(network, parent), + service_(service) {} + +QString SpotifyBaseRequest::service_name() const { + + return service_->name(); + +} + +bool SpotifyBaseRequest::authentication_required() const { + + return true; + +} + +bool SpotifyBaseRequest::authenticated() const { + + return service_->authenticated(); + +} + +bool SpotifyBaseRequest::use_authorization_header() const { + + return true; + +} + +QByteArray SpotifyBaseRequest::authorization_header() const { + + return service_->authorization_header(); + +} QNetworkReply *SpotifyBaseRequest::CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided) { - QUrlQuery url_query; - for (const Param ¶m : params_provided) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QLatin1String(SpotifyService::kApiUrl) + QLatin1Char('/') + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); - - QNetworkReply *reply = network_->get(req); - QObject::connect(reply, &QNetworkReply::sslErrors, this, &SpotifyBaseRequest::HandleSSLErrors); - - qLog(Debug) << "Spotify: Sending request" << url; - - return reply; + return CreateGetRequest(QUrl(QLatin1String(SpotifyService::kApiUrl) + QLatin1Char('/') + ressource_name), params_provided); } -void SpotifyBaseRequest::HandleSSLErrors(const QList &ssl_errors) { +JsonBaseRequest::JsonObjectResult SpotifyBaseRequest::ParseJsonObject(QNetworkReply *reply) { - for (const QSslError &ssl_error : ssl_errors) { - Error(ssl_error.errorString()); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } -} - -QByteArray SpotifyBaseRequest::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("error"_L1) && json_object["error"_L1].isObject()) { + const QJsonObject object_error = json_object["error"_L1].toObject(); + if (object_error.contains("status"_L1) && object_error.contains("message"_L1)) { + const int status = object_error["status"_L1].toInt(); + const QString message = object_error["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(status); + } + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "error". - data = reply->readAll(); - QString error; - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - int status = 0; - 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("error"_L1) && json_obj["error"_L1].isObject()) { - QJsonObject obj_error = json_obj["error"_L1].toObject(); - if (!obj_error.isEmpty() && obj_error.contains("status"_L1) && obj_error.contains("message"_L1)) { - status = obj_error["status"_L1].toInt(); - QString user_message = obj_error["message"_L1].toString(); - error = QStringLiteral("%1 (%2)").arg(user_message).arg(status); - } - } - } - if (error.isEmpty()) { - if (reply->error() == QNetworkReply::NoError) { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - else { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; - -} - -QJsonObject SpotifyBaseRequest::ExtractJsonObj(const QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error(u"Reply from server missing Json data."_s, data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - Error(u"Received empty Json document."_s, data); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); - return QJsonObject(); - } - - return json_obj; - -} - -QJsonValue SpotifyBaseRequest::ExtractItems(const QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj); - -} - -QJsonValue SpotifyBaseRequest::ExtractItems(const QJsonObject &json_obj) { - - if (!json_obj.contains("items"_L1)) { - Error(u"Json reply is missing items."_s, json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"_L1]; - return json_items; - -} - -QString SpotifyBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + "
"_L1; - } - return error_html; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code < 200 || result.http_status_code > 207) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + service_->ClearSession(); + } + + return result; } diff --git a/src/spotify/spotifybaserequest.h b/src/spotify/spotifybaserequest.h index 03b4f6433..296eab834 100644 --- a/src/spotify/spotifybaserequest.h +++ b/src/spotify/spotifybaserequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,28 +22,16 @@ #include "config.h" -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include "includes/shared_ptr.h" - -#include "spotifyservice.h" +#include "core/jsonbaserequest.h" class QNetworkReply; class NetworkAccessManager; +class SpotifyService; -class SpotifyBaseRequest : public QObject { +class SpotifyBaseRequest : public JsonBaseRequest { Q_OBJECT public: @@ -61,32 +49,17 @@ class SpotifyBaseRequest : public QObject { }; protected: - using Param = QPair; - using ParamList = QList; + QString service_name() const override; + bool authentication_required() const override; + bool authenticated() const override; + bool use_authorization_header() const override; + QByteArray authorization_header() const override; QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(const QByteArray &data); - QJsonValue ExtractItems(const QByteArray &data); - QJsonValue ExtractItems(const QJsonObject &json_obj); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - static QString ErrorsToHTML(const QStringList &errors); - - int artistssearchlimit() const { return service_->artistssearchlimit(); } - int albumssearchlimit() const { return service_->albumssearchlimit(); } - int songssearchlimit() const { return service_->songssearchlimit(); } - - QString access_token() const { return service_->access_token(); } - - bool authenticated() const { return service_->authenticated(); } - - private Q_SLOTS: - void HandleSSLErrors(const QList &ssl_errors); - - private: + protected: SpotifyService *service_; - const SharedPtr network_; }; #endif // SPOTIFYBASEREQUEST_H diff --git a/src/spotify/spotifyfavoriterequest.cpp b/src/spotify/spotifyfavoriterequest.cpp index 6a2c94a30..1c692f656 100644 --- a/src/spotify/spotifyfavoriterequest.cpp +++ b/src/spotify/spotifyfavoriterequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * 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,12 +19,6 @@ #include "config.h" -#include -#include -#include -#include -#include -#include #include #include #include @@ -46,20 +40,7 @@ using namespace Qt::Literals::StringLiterals; SpotifyFavoriteRequest::SpotifyFavoriteRequest(SpotifyService *service, const SharedPtr network, QObject *parent) - : SpotifyBaseRequest(service, network, parent), - service_(service), - network_(network) {} - -SpotifyFavoriteRequest::~SpotifyFavoriteRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} + : SpotifyBaseRequest(service, network, parent) {} QString SpotifyFavoriteRequest::FavoriteText(const FavoriteType type) { @@ -121,8 +102,8 @@ void SpotifyFavoriteRequest::AddFavorites(const FavoriteType type, const SongLis if (list_ids.isEmpty() || array_ids.isEmpty()) return; - QByteArray json_data = QJsonDocument(array_ids).toJson(); - QString ids_list = list_ids.join(u','); + const QByteArray json_data = QJsonDocument(array_ids).toJson(); + const QString ids_list = list_ids.join(u','); AddFavoritesRequest(type, ids_list, json_data, songs); @@ -137,16 +118,18 @@ void SpotifyFavoriteRequest::AddFavoritesRequest(const FavoriteType type, const url_query.addQueryItem(u"ids"_s, ids_list); url.setQuery(url_query); } - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + if (service_->authenticated()) { + network_request.setRawHeader("Authorization", service_->authorization_header()); + } QNetworkReply *reply = nullptr; if (type == FavoriteType_Artists) { - reply = network_->put(req, ""); + reply = network_->put(network_request, ""); } else { - reply = network_->put(req, json_data); + reply = network_->put(network_request, json_data); } QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, type, songs]() { AddFavoritesReply(reply, type, songs); }); replies_ << reply; @@ -160,8 +143,9 @@ void SpotifyFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const Favor QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - GetReplyData(reply); - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } @@ -233,8 +217,8 @@ void SpotifyFavoriteRequest::RemoveFavorites(const FavoriteType type, const Song if (list_ids.isEmpty() || array_ids.isEmpty()) return; - QByteArray json_data = QJsonDocument(array_ids).toJson(); - QString ids_list = list_ids.join(u','); + const QByteArray json_data = QJsonDocument(array_ids).toJson(); + const QString ids_list = list_ids.join(u','); RemoveFavoritesRequest(type, ids_list, json_data, songs); @@ -251,17 +235,19 @@ void SpotifyFavoriteRequest::RemoveFavoritesRequest(const FavoriteType type, con url_query.addQueryItem(u"ids"_s, ids_list); url.setQuery(url_query); } - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + if (service_->authenticated()) { + network_request.setRawHeader("Authorization", service_->authorization_header()); + } QNetworkReply *reply = nullptr; if (type == FavoriteType_Artists) { - reply = network_->deleteResource(req); + reply = network_->deleteResource(network_request); } else { // FIXME - reply = network_->deleteResource(req); + reply = network_->deleteResource(network_request); } QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, type, songs]() { RemoveFavoritesReply(reply, type, songs); }); replies_ << reply; @@ -275,8 +261,9 @@ void SpotifyFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Fa QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - GetReplyData(reply); - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } @@ -300,10 +287,3 @@ void SpotifyFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Fa } } - -void SpotifyFavoriteRequest::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "Spotify:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/spotify/spotifyfavoriterequest.h b/src/spotify/spotifyfavoriterequest.h index 181ff9fb2..a63cb4c5a 100644 --- a/src/spotify/spotifyfavoriterequest.h +++ b/src/spotify/spotifyfavoriterequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,7 +43,6 @@ class SpotifyFavoriteRequest : public SpotifyBaseRequest { public: explicit SpotifyFavoriteRequest(SpotifyService *service, const SharedPtr network, QObject *parent = nullptr); - ~SpotifyFavoriteRequest() override; enum FavoriteType { FavoriteType_Artists, @@ -75,18 +74,12 @@ class SpotifyFavoriteRequest : public SpotifyBaseRequest { void RemoveSongs(const SongMap &songs); private: - void Error(const QString &error, const QVariant &debug = QVariant()) override; static QString FavoriteText(const FavoriteType type); void AddFavorites(const FavoriteType type, const SongList &songs); void AddFavoritesRequest(const FavoriteType type, const QString &ids_list, const QByteArray &json_data, const SongList &songs); void RemoveFavorites(const FavoriteType type, const SongList &songs); void RemoveFavorites(const FavoriteType type, const QString &id, const SongList &songs); void RemoveFavoritesRequest(const FavoriteType type, const QString &ids_list, const QByteArray &json_data, const SongList &songs); - - SpotifyService *service_; - const SharedPtr network_; - QList replies_; - }; #endif // SPOTIFYFAVORITEREQUEST_H diff --git a/src/spotify/spotifyrequest.cpp b/src/spotify/spotifyrequest.cpp index 1c9db53f8..25fb6363b 100644 --- a/src/spotify/spotifyrequest.cpp +++ b/src/spotify/spotifyrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,7 +21,6 @@ #include -#include #include #include #include @@ -33,13 +32,14 @@ #include #include #include +#include -#include "core/logging.h" -#include "core/networkaccessmanager.h" -#include "core/song.h" #include "constants/timeconstants.h" #include "utilities/imageutils.h" #include "utilities/coverutils.h" +#include "core/logging.h" +#include "core/networkaccessmanager.h" +#include "core/song.h" #include "spotifyservice.h" #include "spotifybaserequest.h" #include "spotifyrequest.h" @@ -47,18 +47,17 @@ using namespace Qt::Literals::StringLiterals; namespace { -const int kMaxConcurrentArtistsRequests = 1; -const int kMaxConcurrentAlbumsRequests = 1; -const int kMaxConcurrentSongsRequests = 1; -const int kMaxConcurrentArtistAlbumsRequests = 1; -const int kMaxConcurrentAlbumSongsRequests = 1; -const int kMaxConcurrentAlbumCoverRequests = 10; -const int kFlushRequestsDelay = 200; -} +constexpr int kMaxConcurrentArtistsRequests = 1; +constexpr int kMaxConcurrentAlbumsRequests = 1; +constexpr int kMaxConcurrentSongsRequests = 1; +constexpr int kMaxConcurrentArtistAlbumsRequests = 1; +constexpr int kMaxConcurrentAlbumSongsRequests = 1; +constexpr int kMaxConcurrentAlbumCoverRequests = 10; +constexpr int kFlushRequestsDelay = 200; +} // namespace SpotifyRequest::SpotifyRequest(SpotifyService *service, const SharedPtr network, const Type type, QObject *parent) : SpotifyBaseRequest(service, network, parent), - service_(service), network_(network), timer_flush_requests_(new QTimer(this)), type_(type), @@ -107,20 +106,6 @@ SpotifyRequest::~SpotifyRequest() { timer_flush_requests_->stop(); } - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - - while (!album_cover_replies_.isEmpty()) { - QNetworkReply *reply = album_cover_replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - } void SpotifyRequest::Process() { @@ -232,7 +217,7 @@ void SpotifyRequest::FlushArtistsRequests() { while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { - Request request = artists_requests_queue_.dequeue(); + const Request request = artists_requests_queue_.dequeue(); ParamList parameters = ParamList() << Param(u"type"_s, u"artist"_s); if (type_ == Type::SearchArtists) { @@ -252,7 +237,6 @@ void SpotifyRequest::FlushArtistsRequests() { reply = CreateRequest(u"search"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { ArtistsReplyReceived(reply, request.limit, request.offset); }); ++artists_requests_active_; @@ -286,7 +270,7 @@ void SpotifyRequest::FlushAlbumsRequests() { while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - Request request = albums_requests_queue_.dequeue(); + const Request request = albums_requests_queue_.dequeue(); ParamList parameters; if (type_ == Type::SearchAlbums) { @@ -306,7 +290,6 @@ void SpotifyRequest::FlushAlbumsRequests() { reply = CreateRequest(u"search"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumsReplyReceived(reply, request.limit, request.offset); }); ++albums_requests_active_; @@ -340,7 +323,7 @@ void SpotifyRequest::FlushSongsRequests() { while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { - Request request = songs_requests_queue_.dequeue(); + const Request request = songs_requests_queue_.dequeue(); ParamList parameters; if (type_ == Type::SearchSongs) { @@ -361,7 +344,6 @@ void SpotifyRequest::FlushSongsRequests() { reply = CreateRequest(u"search"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { SongsReplyReceived(reply, request.limit, request.offset); }); ++songs_requests_active_; @@ -419,57 +401,55 @@ void SpotifyRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_ QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); --artists_requests_active_; ++artists_requests_received_; if (finished_) return; - if (data.isEmpty()) { - ArtistsFinishCheck(); + int offset = 0; + int artists_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, limit_requested, &offset, &artists_received]() { ArtistsFinishCheck(limit_requested, offset, artists_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - ArtistsFinishCheck(); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("artists"_L1) || !json_obj["artists"_L1].isObject()) { - Error(u"Json object missing values."_s, json_obj); - ArtistsFinishCheck(); + if (!json_object.contains("artists"_L1) || !json_object["artists"_L1].isObject()) { + Error(u"Json object missing values."_s, json_object); return; } - QJsonObject obj_artists = json_obj["artists"_L1].toObject(); + const QJsonObject obj_artists = json_object["artists"_L1].toObject(); if (!obj_artists.contains("limit"_L1) || !obj_artists.contains("total"_L1) || !obj_artists.contains("items"_L1)) { Error(u"Json object missing values."_s, obj_artists); - ArtistsFinishCheck(); return; } - int offset = 0; if (obj_artists.contains("offset"_L1)) { offset = obj_artists["offset"_L1].toInt(); } - int artists_total = obj_artists["total"_L1].toInt(); + const int artists_total = obj_artists["total"_L1].toInt(); if (offset_requested == 0) { artists_total_ = artists_total; } else if (artists_total != artists_total_) { Error(QStringLiteral("Total returned does not match previous total! %1 != %2").arg(artists_total).arg(artists_total_)); - ArtistsFinishCheck(); return; } if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - ArtistsFinishCheck(); return; } @@ -477,20 +457,18 @@ void SpotifyRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_ Q_EMIT UpdateProgress(query_id_, GetProgress(artists_received_, artists_total_)); } - QJsonValue value_items = ExtractItems(obj_artists); - if (!value_items.isArray()) { - ArtistsFinishCheck(); + const JsonArrayResult json_array_result = GetJsonArray(obj_artists, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { // Empty array means no results if (offset_requested == 0) no_results_ = true; - ArtistsFinishCheck(); return; } - int artists_received = 0; for (const QJsonValue &value_item : array_items) { ++artists_received; @@ -499,24 +477,24 @@ void SpotifyRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_ Error(u"Invalid Json reply, item in array is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1)) { - QJsonValue json_item = obj_item["item"_L1]; + if (object_item.contains("item"_L1)) { + QJsonValue json_item = object_item["item"_L1]; if (!json_item.isObject()) { Error(u"Invalid Json reply, item in array is not a object."_s, json_item); continue; } - obj_item = json_item.toObject(); + object_item = json_item.toObject(); } - if (!obj_item.contains("id"_L1) || !obj_item.contains("name"_L1)) { - Error(u"Invalid Json reply, item missing id or album."_s, obj_item); + if (!object_item.contains("id"_L1) || !object_item.contains("name"_L1)) { + Error(u"Invalid Json reply, item missing id or album."_s, object_item); continue; } - QString artist_id = obj_item["id"_L1].toString(); - QString artist = obj_item["name"_L1].toString(); + const QString artist_id = object_item["id"_L1].toString(); + const QString artist = object_item["name"_L1].toString(); if (artist_albums_requests_pending_.contains(artist_id)) continue; @@ -530,15 +508,13 @@ void SpotifyRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_ if (offset_requested != 0) Q_EMIT UpdateProgress(query_id_, GetProgress(artists_total_, artists_received_)); - ArtistsFinishCheck(limit_requested, offset, artists_received); - } void SpotifyRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { if (finished_) return; - if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) { + if (artists_received > 0 && (limit == 0 || limit > artists_received) && artists_received_ < artists_total_) { int offset_next = offset + artists_received; if (offset_next > 0 && offset_next < artists_total_) { if (type_ == Type::FavouriteArtists) AddArtistsRequest(offset_next); @@ -592,13 +568,12 @@ void SpotifyRequest::FlushArtistAlbumsRequests() { while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) { - ArtistAlbumsRequest request = artist_albums_requests_queue_.dequeue(); + const ArtistAlbumsRequest request = artist_albums_requests_queue_.dequeue(); ParamList parameters; if (request.offset > 0) parameters << Param(u"offset"_s, QString::number(request.offset)); QNetworkReply *reply = CreateRequest(QStringLiteral("artists/%1/albums").arg(request.artist.artist_id), parameters); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { ArtistAlbumsReplyReceived(reply, request.artist, request.offset); }); - replies_ << reply; ++artist_albums_requests_active_; @@ -622,40 +597,43 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - AlbumsFinishCheck(artist_artist); + int offset = 0; + int albums_total = 0; + int albums_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, artist_artist, limit_requested, &offset, &albums_total, &albums_received]() { AlbumsFinishCheck(artist_artist, limit_requested, offset, albums_total, albums_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(artist_artist); + QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (json_obj.contains("albums"_L1) && json_obj["albums"_L1].isObject()) { - json_obj = json_obj["albums"_L1].toObject(); + if (json_object.contains("albums"_L1) && json_object["albums"_L1].isObject()) { + json_object = json_object["albums"_L1].toObject(); } - if (json_obj.contains("tracks"_L1) && json_obj["tracks"_L1].isObject()) { - json_obj = json_obj["tracks"_L1].toObject(); + if (json_object.contains("tracks"_L1) && json_object["tracks"_L1].isObject()) { + json_object = json_object["tracks"_L1].toObject(); } - if (!json_obj.contains("limit"_L1) || - !json_obj.contains("offset"_L1) || - !json_obj.contains("total"_L1) || - !json_obj.contains("items"_L1)) { - Error(u"Json object missing values."_s, json_obj); - AlbumsFinishCheck(artist_artist); + if (!json_object.contains("limit"_L1) || + !json_object.contains("offset"_L1) || + !json_object.contains("total"_L1) || + !json_object.contains("items"_L1)) { + Error(u"Json object missing values."_s, json_object); return; } - int offset = json_obj["offset"_L1].toInt(); - int albums_total = json_obj["total"_L1].toInt(); + offset = json_object["offset"_L1].toInt(); + albums_total = json_object["total"_L1].toInt(); if (type_ == Type::FavouriteAlbums || type_ == Type::SearchAlbums) { albums_total_ = albums_total; @@ -663,73 +641,70 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - AlbumsFinishCheck(artist_artist); return; } - QJsonValue value_items = ExtractItems(json_obj); - if (!value_items.isArray()) { - AlbumsFinishCheck(artist_artist); + const JsonArrayResult json_array_result = GetJsonArray(json_object, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { if ((type_ == Type::FavouriteAlbums || type_ == Type::SearchAlbums || (type_ == Type::SearchSongs && fetchalbums_)) && offset_requested == 0) { no_results_ = true; } - AlbumsFinishCheck(artist_artist); return; } - int albums_received = 0; - for (const QJsonValue &value_item : array_items) { + albums_received = static_cast(array_items.count()); - ++albums_received; + for (const QJsonValue &value_item : array_items) { if (!value_item.isObject()) { Error(u"Invalid Json reply, item in array is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1)) { - QJsonValue json_item = obj_item["item"_L1]; + if (object_item.contains("item"_L1)) { + QJsonValue json_item = object_item["item"_L1]; if (!json_item.isObject()) { Error(u"Invalid Json reply, item in array is not a object."_s, json_item); continue; } - obj_item = json_item.toObject(); + object_item = json_item.toObject(); } - if (obj_item.contains("album"_L1)) { - QJsonValue json_item = obj_item["album"_L1]; + if (object_item.contains("album"_L1)) { + QJsonValue json_item = object_item["album"_L1]; if (!json_item.isObject()) { Error(u"Invalid Json reply, album in array is not a object."_s, json_item); continue; } - obj_item = json_item.toObject(); + object_item = json_item.toObject(); } Artist artist; Album album; - if (!obj_item.contains("id"_L1)) { - Error(u"Invalid Json reply, item is missing ID."_s, obj_item); + if (!object_item.contains("id"_L1)) { + Error(u"Invalid Json reply, item is missing ID."_s, object_item); continue; } - if (!obj_item.contains("name"_L1)) { - Error(u"Invalid Json reply, item is missing name."_s, obj_item); + if (!object_item.contains("name"_L1)) { + Error(u"Invalid Json reply, item is missing name."_s, object_item); continue; } - if (!obj_item.contains("images"_L1)) { - Error(u"Invalid Json reply, item is missing images."_s, obj_item); + if (!object_item.contains("images"_L1)) { + Error(u"Invalid Json reply, item is missing images."_s, object_item); continue; } - album.album_id = obj_item["id"_L1].toString(); - album.album = obj_item["name"_L1].toString(); + album.album_id = object_item["id"_L1].toString(); + album.album = object_item["name"_L1].toString(); - if (obj_item.contains("artists"_L1) && obj_item["artists"_L1].isArray()) { - const QJsonArray array_artists = obj_item["artists"_L1].toArray(); + if (object_item.contains("artists"_L1) && object_item["artists"_L1].isArray()) { + const QJsonArray array_artists = object_item["artists"_L1].toArray(); bool artist_matches = false; for (const QJsonValue &value : array_artists) { if (!value.isObject()) { @@ -747,7 +722,6 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a } } if (!artist_matches && (type_ == Type::FavouriteArtists || type_ == Type::SearchArtists)) { - AlbumsFinishCheck(artist_artist); return; } } @@ -756,8 +730,8 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a artist = artist_artist; } - if (obj_item.contains("images"_L1) && obj_item["images"_L1].isArray()) { - const QJsonArray array_images = obj_item["images"_L1].toArray(); + if (object_item.contains("images"_L1) && object_item["images"_L1].isArray()) { + const QJsonArray array_images = object_item["images"_L1].toArray(); for (const QJsonValue &value : array_images) { if (!value.isObject()) { continue; @@ -773,8 +747,8 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a } } - if (obj_item.contains("tracks"_L1) && obj_item["tracks"_L1].isObject()) { - QJsonObject obj_tracks = obj_item["tracks"_L1].toObject(); + if (object_item.contains("tracks"_L1) && object_item["tracks"_L1].isObject()) { + QJsonObject obj_tracks = object_item["tracks"_L1].toObject(); if (obj_tracks.contains("items"_L1) && obj_tracks["items"_L1].isArray()) { const QJsonArray array_tracks = obj_tracks["items"_L1].toArray(); bool compilation = false; @@ -816,15 +790,13 @@ void SpotifyRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_a Q_EMIT UpdateProgress(query_id_, GetProgress(albums_received_, albums_total_)); } - AlbumsFinishCheck(artist_artist, limit_requested, offset, albums_total, albums_received); - } void SpotifyRequest::AlbumsFinishCheck(const Artist &artist, const int limit, const int offset, const int albums_total, const int albums_received) { if (finished_) return; - if (limit == 0 || limit > albums_received) { + if (albums_received > 0 && (limit == 0 || limit > albums_received)) { int offset_next = offset + albums_received; if (offset_next > 0 && offset_next < albums_total) { switch (type_) { @@ -904,15 +876,12 @@ void SpotifyRequest::AddAlbumSongsRequest(const Artist &artist, const Album &alb void SpotifyRequest::FlushAlbumSongsRequests() { while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { - - AlbumSongsRequest request = album_songs_requests_queue_.dequeue(); + const AlbumSongsRequest request = album_songs_requests_queue_.dequeue(); ++album_songs_requests_active_; ParamList parameters; if (request.offset > 0) parameters << Param(u"offset"_s, QString::number(request.offset)); QNetworkReply *reply = CreateRequest(QStringLiteral("albums/%1/tracks").arg(request.album.album_id), parameters); - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumSongsReplyReceived(reply, request.artist, request.album, request.offset); }); - } } @@ -935,36 +904,38 @@ void SpotifyRequest::SongsReceived(QNetworkReply *reply, const Artist &artist, c QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested, 0, 0); + int songs_total = 0; + int songs_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, artist, album, limit_requested, offset_requested, &songs_total, &songs_received]() { SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, songs_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested, 0, 0); + QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (json_obj.contains("tracks"_L1) && json_obj["tracks"_L1].isObject()) { - json_obj = json_obj["tracks"_L1].toObject(); + if (json_object.contains("tracks"_L1) && json_object["tracks"_L1].isObject()) { + json_object = json_object["tracks"_L1].toObject(); } - if (!json_obj.contains("limit"_L1) || - !json_obj.contains("offset"_L1) || - !json_obj.contains("total"_L1) || - !json_obj.contains("items"_L1)) { - Error(u"Json object missing values."_s, json_obj); - SongsFinishCheck(artist, album, limit_requested, offset_requested, 0, 0); + if (!json_object.contains("limit"_L1) || + !json_object.contains("offset"_L1) || + !json_object.contains("total"_L1) || + !json_object.contains("items"_L1)) { + Error(u"Json object missing values."_s, json_object); return; } - int offset = json_obj["offset"_L1].toInt(); - int songs_total = json_obj["total"_L1].toInt(); + const int offset = json_object["offset"_L1].toInt(); + songs_total = json_object["total"_L1].toInt(); if (type_ == Type::FavouriteSongs || type_ == Type::SearchSongs) { songs_total_ = songs_total; @@ -972,48 +943,45 @@ void SpotifyRequest::SongsReceived(QNetworkReply *reply, const Artist &artist, c if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); return; } - QJsonValue json_value = ExtractItems(json_obj); - if (!json_value.isArray()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); + const JsonArrayResult json_array_result = GetJsonArray(json_object, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = json_value.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { if ((type_ == Type::FavouriteSongs || type_ == Type::SearchSongs) && offset_requested == 0) { no_results_ = true; } - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); return; } bool compilation = false; bool multidisc = false; SongList songs; - int songs_received = 0; for (const QJsonValue &value_item : array_items) { if (!value_item.isObject()) { Error(u"Invalid Json reply, track is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1) && obj_item["item"_L1].isObject()) { - obj_item = obj_item["item"_L1].toObject(); + if (object_item.contains("item"_L1) && object_item["item"_L1].isObject()) { + object_item = object_item["item"_L1].toObject(); } - if (obj_item.contains("track"_L1) && obj_item["track"_L1].isObject()) { - obj_item = obj_item["track"_L1].toObject(); + if (object_item.contains("track"_L1) && object_item["track"_L1].isObject()) { + object_item = object_item["track"_L1].toObject(); } ++songs_received; Song song(Song::Source::Spotify); - ParseSong(song, obj_item, artist, album); + ParseSong(song, object_item, artist, album); if (!song.is_valid()) continue; if (song.disc() >= 2) multidisc = true; if (song.is_compilation()) compilation = true; @@ -1031,15 +999,13 @@ void SpotifyRequest::SongsReceived(QNetworkReply *reply, const Artist &artist, c Q_EMIT UpdateProgress(query_id_, GetProgress(songs_received_, songs_total_)); } - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, songs_received); - } void SpotifyRequest::SongsFinishCheck(const Artist &artist, const Album &album, const int limit, const int offset, const int songs_total, const int songs_received) { if (finished_) return; - if (limit == 0 || limit > songs_received) { + if (songs_received > 0 && (limit == 0 || limit > songs_received)) { int offset_next = offset + songs_received; if (offset_next > 0 && offset_next < songs_total) { switch (type_) { @@ -1240,25 +1206,18 @@ void SpotifyRequest::AddAlbumCoverRequest(const Song &song) { void SpotifyRequest::FlushAlbumCoverRequests() { while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { - - AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); - - QNetworkRequest req(request.url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - album_cover_replies_ << reply; + const AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); + QNetworkReply *reply = CreateGetRequest(request.url); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumCoverReceived(reply, request.album_id, request.url, request.filename); }); - ++album_covers_requests_active_; - } } void SpotifyRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename) { - if (album_cover_replies_.contains(reply)) { - album_cover_replies_.removeAll(reply); + if (replies_.contains(reply)) { + replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); } @@ -1272,24 +1231,23 @@ void SpotifyRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &alb if (finished_) return; + const QScopeGuard finish_check = qScopeGuard([this]() { AlbumCoverFinishCheck(); }); + Q_EMIT UpdateProgress(query_id_, GetProgress(album_covers_requests_received_, album_covers_requests_total_)); if (!album_covers_requests_sent_.contains(album_id)) { - AlbumCoverFinishCheck(); return; } if (reply->error() != QNetworkReply::NoError) { Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { Error(QStringLiteral("Received HTTP code %1 for %2.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()).arg(url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1300,7 +1258,6 @@ void SpotifyRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &alb if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) { Error(QStringLiteral("Unsupported mimetype for image reader %1 for %2").arg(mimetype, url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1308,7 +1265,6 @@ void SpotifyRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &alb if (data.isEmpty()) { Error(QStringLiteral("Received empty image data for %1").arg(url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1338,8 +1294,6 @@ void SpotifyRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &alb Error(QStringLiteral("Error decoding image data from %1").arg(url.toString())); } - AlbumCoverFinishCheck(); - } void SpotifyRequest::AlbumCoverFinishCheck() { @@ -1373,17 +1327,19 @@ void SpotifyRequest::FinishCheck() { } finished_ = true; if (no_results_ && songs_.isEmpty()) { - if (IsSearch()) + if (IsSearch()) { Q_EMIT Results(query_id_, SongMap(), tr("No match.")); - else + } + else { Q_EMIT Results(query_id_, SongMap(), QString()); + } } else { - if (songs_.isEmpty() && errors_.isEmpty()) { + if (songs_.isEmpty() && error_.isEmpty()) { Q_EMIT Results(query_id_, songs_, tr("Data missing error")); } else { - Q_EMIT Results(query_id_, songs_, ErrorsToHTML(errors_)); + Q_EMIT Results(query_id_, songs_, error_); } } } @@ -1396,22 +1352,20 @@ int SpotifyRequest::GetProgress(const int count, const int total) { } -void SpotifyRequest::Error(const QString &error, const QVariant &debug) { +void SpotifyRequest::Error(const QString &error_message, const QVariant &debug_output) { - if (!error.isEmpty()) { - errors_ << error; - qLog(Error) << "Spotify:" << error; + qLog(Error) << "Spotify:" << error_message; + if (debug_output.isValid()) { + qLog(Debug) << debug_output; } - if (debug.isValid()) qLog(Debug) << debug; - - FinishCheck(); + error_ = error_message; } -void SpotifyRequest::Warn(const QString &error, const QVariant &debug) { +void SpotifyRequest::Warn(const QString &error_message, const QVariant &debug) { - qLog(Error) << "Spotify:" << error; + qLog(Error) << "Spotify:" << error_message; if (debug.isValid()) qLog(Debug) << debug; } diff --git a/src/spotify/spotifyrequest.h b/src/spotify/spotifyrequest.h index de96118fa..f0004be35 100644 --- a/src/spotify/spotifyrequest.h +++ b/src/spotify/spotifyrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,11 +22,6 @@ #include "config.h" -#include -#include -#include -#include -#include #include #include #include @@ -36,6 +31,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/song.h" @@ -138,9 +134,9 @@ class SpotifyRequest : public SpotifyBaseRequest { void AddSongsSearchRequest(const int offset = 0); void FlushSongsRequests(); - void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0); - void AlbumsFinishCheck(const Artist &artist, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0); - void SongsFinishCheck(const Artist &artist, const Album &album, const int limit = 0, const int offset = 0, const int songs_total = 0, const int songs_received = 0); + void ArtistsFinishCheck(const int limit, const int offset, const int artists_received); + void AlbumsFinishCheck(const Artist &artist, const int limit, const int offset, const int albums_total, const int albums_received); + void SongsFinishCheck(const Artist &artist, const Album &album, const int limit, const int offset, const int songs_total, const int songs_received); void AddArtistAlbumsRequest(const Artist &artist, const int offset = 0); void FlushArtistAlbumsRequests(); @@ -158,11 +154,10 @@ class SpotifyRequest : public SpotifyBaseRequest { int GetProgress(const int count, const int total); void FinishCheck(); - static void Warn(const QString &error, const QVariant &debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; + void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override; + void Warn(const QString &error_message, const QVariant &debug); private: - SpotifyService *service_; const SharedPtr network_; QTimer *timer_flush_requests_; @@ -222,11 +217,10 @@ class SpotifyRequest : public SpotifyBaseRequest { int album_covers_requests_received_; SongMap songs_; - QStringList errors_; bool no_results_; - QList replies_; - QList album_cover_replies_; - + QString error_; }; +using SpotifyRequestPtr = QScopedPointer; + #endif // SPOTIFYREQUEST_H diff --git a/src/spotify/spotifyservice.cpp b/src/spotify/spotifyservice.cpp index 142801077..a0a2299d7 100644 --- a/src/spotify/spotifyservice.cpp +++ b/src/spotify/spotifyservice.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * 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,41 +19,21 @@ #include "config.h" -#include #include -#include -#include -#include -#include #include -#include -#include -#include #include -#include #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include "constants/spotifysettings.h" #include "core/logging.h" #include "core/song.h" #include "core/settings.h" #include "core/taskmanager.h" #include "core/database.h" #include "core/networkaccessmanager.h" -#include "core/localredirectserver.h" -#include "constants/timeconstants.h" -#include "utilities/randutils.h" +#include "core/oauthenticator.h" #include "streaming/streamingsearchview.h" #include "collection/collectionbackend.h" #include "collection/collectionmodel.h" @@ -61,7 +41,6 @@ #include "spotifybaserequest.h" #include "spotifyrequest.h" #include "spotifyfavoriterequest.h" -#include "constants/spotifysettings.h" using namespace Qt::Literals::StringLiterals; @@ -73,9 +52,9 @@ namespace { constexpr char kOAuthAuthorizeUrl[] = "https://accounts.spotify.com/authorize"; constexpr char kOAuthAccessTokenUrl[] = "https://accounts.spotify.com/api/token"; constexpr char kOAuthRedirectUrl[] = "http://localhost:63111/"; +constexpr char kOAuthScope[] = "user-follow-read user-follow-modify user-library-read user-library-modify streaming"; constexpr char kClientIDB64[] = "ZTZjY2Y2OTQ5NzY1NGE3NThjOTAxNWViYzdiMWQzMTc="; constexpr char kClientSecretB64[] = "N2ZlMDMxODk1NTBlNDE3ZGI1ZWQ1MzE3ZGZlZmU2MTE="; - constexpr char kArtistsSongsTable[] = "spotify_artists_songs"; constexpr char kAlbumsSongsTable[] = "spotify_albums_songs"; constexpr char kSongsTable[] = "spotify_songs"; @@ -92,6 +71,7 @@ SpotifyService::SpotifyService(const SharedPtr task_manager, QObject *parent) : StreamingService(Song::Source::Spotify, u"Spotify"_s, u"spotify"_s, QLatin1String(SpotifySettings::kSettingsGroup), parent), network_(network), + oauth_(new OAuthenticator(network, this)), artists_collection_backend_(nullptr), albums_collection_backend_(nullptr), songs_collection_backend_(nullptr), @@ -99,7 +79,6 @@ SpotifyService::SpotifyService(const SharedPtr task_manager, albums_collection_model_(nullptr), songs_collection_model_(nullptr), timer_search_delay_(new QTimer(this)), - timer_refresh_login_(new QTimer(this)), favorite_request_(new SpotifyFavoriteRequest(this, network_, this)), enabled_(false), artistssearchlimit_(1), @@ -107,13 +86,22 @@ SpotifyService::SpotifyService(const SharedPtr task_manager, songssearchlimit_(1), fetchalbums_(true), download_album_covers_(true), - expires_in_(0), - login_time_(0), pending_search_id_(0), next_pending_search_id_(1), pending_search_type_(SearchType::Artists), - search_id_(0), - server_(nullptr) { + search_id_(0) { + + oauth_->set_settings_group(QLatin1String(SpotifySettings::kSettingsGroup)); + oauth_->set_type(OAuthenticator::Type::Authorization_Code); + oauth_->set_authorize_url(QUrl(QLatin1String(kOAuthAuthorizeUrl))); + oauth_->set_redirect_url(QUrl(QLatin1String(kOAuthRedirectUrl))); + oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl))); + oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64))); + oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64))); + oauth_->set_scope(QLatin1String(kOAuthScope)); + oauth_->set_use_local_redirect_server(true); + oauth_->set_random_port(false); + QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &SpotifyService::OAuthFinished); // Backends @@ -134,9 +122,6 @@ SpotifyService::SpotifyService(const SharedPtr task_manager, albums_collection_model_ = new CollectionModel(albums_collection_backend_, albumcover_loader, this); songs_collection_model_ = new CollectionModel(songs_collection_backend_, albumcover_loader, this); - timer_refresh_login_->setSingleShot(true); - QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &SpotifyService::RequestNewAccessToken); - timer_search_delay_->setSingleShot(true); QObject::connect(timer_search_delay_, &QTimer::timeout, this, &SpotifyService::StartSearch); @@ -158,19 +143,12 @@ SpotifyService::SpotifyService(const SharedPtr task_manager, QObject::connect(favorite_request_, &SpotifyFavoriteRequest::SongsRemoved, &*songs_collection_backend_, &CollectionBackend::DeleteSongs); SpotifyService::ReloadSettings(); - LoadSession(); + oauth_->LoadSession(); } SpotifyService::~SpotifyService() { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - artists_collection_backend_->deleteLater(); albums_collection_backend_->deleteLater(); songs_collection_backend_->deleteLater(); @@ -201,25 +179,15 @@ void SpotifyService::ExitReceived() { } -void SpotifyService::LoadSession() { +bool SpotifyService::authenticated() const { - refresh_login_timer_.setSingleShot(true); - QObject::connect(&refresh_login_timer_, &QTimer::timeout, this, &SpotifyService::RequestNewAccessToken); + return oauth_->authenticated(); - Settings s; - s.beginGroup(SpotifySettings::kSettingsGroup); - access_token_ = s.value(SpotifySettings::kAccessToken).toString(); - refresh_token_ = s.value(SpotifySettings::kRefreshToken).toString(); - expires_in_ = s.value(SpotifySettings::kExpiresIn).toLongLong(); - login_time_ = s.value(SpotifySettings::kLoginTime).toLongLong(); - s.endGroup(); +} - if (!refresh_token_.isEmpty()) { - qint64 time = static_cast(expires_in_) - (QDateTime::currentSecsSinceEpoch() - static_cast(login_time_)); - if (time < 1) time = 1; - refresh_login_timer_.setInterval(static_cast(time * kMsecPerSec)); - refresh_login_timer_.start(); - } +QByteArray SpotifyService::authorization_header() const { + + return oauth_->authorization_header(); } @@ -245,267 +213,25 @@ void SpotifyService::ReloadSettings() { void SpotifyService::Authenticate() { - QUrl redirect_url(QString::fromLatin1(kOAuthRedirectUrl)); - - if (!server_) { - server_ = new LocalRedirectServer(this); - int port = redirect_url.port(); - int port_max = port + 10; - bool success = false; - Q_FOREVER { - server_->set_port(port); - if (server_->Listen()) { - success = true; - break; - } - ++port; - if (port > port_max) break; - } - if (!success) { - LoginError(server_->error()); - server_->deleteLater(); - server_ = nullptr; - return; - } - QObject::connect(server_, &LocalRedirectServer::Finished, this, &SpotifyService::RedirectArrived); - } - - code_verifier_ = Utilities::CryptographicRandomString(44); - code_challenge_ = QString::fromLatin1(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding)); - if (code_challenge_.lastIndexOf(u'=') == code_challenge_.length() - 1) { - code_challenge_.chop(1); - } - - const ParamList params = ParamList() << Param(u"client_id"_s, QString::fromLatin1(QByteArray::fromBase64(kClientIDB64))) - << Param(u"response_type"_s, u"code"_s) - << Param(u"redirect_uri"_s, redirect_url.toString()) - << Param(u"state"_s, code_challenge_) - << Param(u"scope"_s, u"user-follow-read user-follow-modify user-library-read user-library-modify streaming"_s); - - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QString::fromLatin1(kOAuthAuthorizeUrl)); - url.setQuery(url_query); - - const bool result = QDesktopServices::openUrl(url); - if (!result) { - QMessageBox messagebox(QMessageBox::Information, tr("Spotify Authentication"), tr("Please open this URL in your browser") + QStringLiteral(":
%1").arg(url.toString()), QMessageBox::Ok); - messagebox.setTextFormat(Qt::RichText); - messagebox.exec(); - } + oauth_->Authenticate(); } -void SpotifyService::Deauthenticate() { +void SpotifyService::ClearSession() { - access_token_.clear(); - refresh_token_.clear(); - expires_in_ = 0; - login_time_ = 0; - - Settings s; - s.beginGroup(SpotifySettings::kSettingsGroup); - s.remove(SpotifySettings::kAccessToken); - s.remove(SpotifySettings::kRefreshToken); - s.remove(SpotifySettings::kExpiresIn); - s.remove(SpotifySettings::kLoginTime); - s.endGroup(); - - refresh_login_timer_.stop(); + oauth_->ClearSession(); } -void SpotifyService::RedirectArrived() { +void SpotifyService::OAuthFinished(const bool success, const QString &error) { - if (!server_) return; - - if (server_->error().isEmpty()) { - QUrl url = server_->request_url(); - if (url.isValid()) { - QUrlQuery url_query(url); - if (url_query.hasQueryItem(u"error"_s)) { - LoginError(QUrlQuery(url).queryItemValue(u"error"_s)); - } - else if (url_query.hasQueryItem(u"code"_s) && url_query.hasQueryItem(u"state"_s)) { - qLog(Debug) << "Spotify: Authorization URL Received" << url; - QString code = url_query.queryItemValue(u"code"_s); - QUrl redirect_url(QString::fromLatin1(kOAuthRedirectUrl)); - redirect_url.setPort(server_->url().port()); - RequestAccessToken(code, redirect_url); - } - else { - LoginError(tr("Redirect missing token code or state!")); - } - } - else { - LoginError(tr("Received invalid reply from web browser.")); - } + if (success) { + Q_EMIT LoginFinished(true); + Q_EMIT LoginSuccess(); } else { - LoginError(server_->error()); - } - - server_->close(); - server_->deleteLater(); - server_ = nullptr; - -} - -void SpotifyService::RequestAccessToken(const QString &code, const QUrl &redirect_url) { - - refresh_login_timer_.stop(); - - ParamList params = ParamList() << Param(u"client_id"_s, QString::fromLatin1(QByteArray::fromBase64(kClientIDB64))) - << Param(u"client_secret"_s, QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64))); - - if (!code.isEmpty() && !redirect_url.isEmpty()) { - params << Param(u"grant_type"_s, u"authorization_code"_s); - params << Param(u"code"_s, code); - params << Param(u"redirect_uri"_s, redirect_url.toString()); - } - else if (!refresh_token_.isEmpty() && enabled_) { - params << Param(u"grant_type"_s, u"refresh_token"_s); - params << Param(u"refresh_token"_s, refresh_token_); - } - else { - return; - } - - QUrlQuery url_query; - for (const Param ¶m : std::as_const(params)) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl new_url(QString::fromLatin1(kOAuthAccessTokenUrl)); - QNetworkRequest req(new_url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - QString auth_header_data = QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)) + QLatin1Char(':') + QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64)); - req.setRawHeader("Authorization", "Basic " + auth_header_data.toUtf8().toBase64()); - - QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - - QNetworkReply *reply = network_->post(req, query); - replies_ << reply; - QObject::connect(reply, &QNetworkReply::sslErrors, this, &SpotifyService::HandleLoginSSLErrors); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AccessTokenRequestFinished(reply); }); - -} - -void SpotifyService::HandleLoginSSLErrors(const QList &ssl_errors) { - - for (const QSslError &ssl_error : ssl_errors) { - login_errors_ += ssl_error.errorString(); - } - -} - -void SpotifyService::AccessTokenRequestFinished(QNetworkReply *reply) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - LoginError(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - return; - } - else { - // See if there is Json data containing "error" and "error_description" 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("error"_L1) && json_obj.contains("error_description"_L1)) { - QString error = json_obj["error"_L1].toString(); - QString error_description = json_obj["error_description"_L1].toString(); - login_errors_ << QStringLiteral("Authentication failure: %1 (%2)").arg(error, error_description); - } - } - 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; - } - } - - QByteArray data = reply->readAll(); - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - LoginError(QStringLiteral("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString())); - return; - } - - if (json_doc.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); - return; - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - LoginError(u"Authentication reply from server has empty Json object."_s, json_doc); - return; - } - - if (!json_obj.contains("access_token"_L1) || !json_obj.contains("expires_in"_L1)) { - LoginError(u"Authentication reply from server is missing access token or expires in."_s, json_obj); - return; - } - - access_token_ = json_obj["access_token"_L1].toString(); - if (json_obj.contains("refresh_token"_L1)) { - refresh_token_ = json_obj["refresh_token"_L1].toString(); - } - expires_in_ = json_obj["expires_in"_L1].toInt(); - login_time_ = QDateTime::currentSecsSinceEpoch(); - - Settings s; - s.beginGroup(SpotifySettings::kSettingsGroup); - s.setValue(SpotifySettings::kAccessToken, access_token_); - s.setValue(SpotifySettings::kRefreshToken, refresh_token_); - s.setValue(SpotifySettings::kExpiresIn, expires_in_); - s.setValue(SpotifySettings::kLoginTime, login_time_); - s.endGroup(); - - if (expires_in_ > 0) { - refresh_login_timer_.setInterval(static_cast(expires_in_ * kMsecPerSec)); - refresh_login_timer_.start(); - } - - qLog(Debug) << "Spotify: Authentication was successful, login expires in" << expires_in_; - - Q_EMIT LoginComplete(true); - Q_EMIT LoginSuccess(); - -} - -void SpotifyService::ResetArtistsRequest() { - - if (artists_request_) { - QObject::disconnect(&*artists_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*artists_request_, nullptr); - artists_request_.reset(); + Q_EMIT LoginFailure(error); + Q_EMIT LoginFinished(false); } } @@ -518,8 +244,7 @@ void SpotifyService::GetArtists() { return; } - ResetArtistsRequest(); - artists_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteArtists, this), [](SpotifyRequest *request) { request->deleteLater(); }); + artists_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteArtists, this)); QObject::connect(&*artists_request_, &SpotifyRequest::Results, this, &SpotifyService::ArtistsResultsReceived); QObject::connect(&*artists_request_, &SpotifyRequest::UpdateStatus, this, &SpotifyService::ArtistsUpdateStatusReceived); QObject::connect(&*artists_request_, &SpotifyRequest::ProgressSetMaximum, this, &SpotifyService::ArtistsProgressSetMaximumReceived); @@ -529,6 +254,12 @@ void SpotifyService::GetArtists() { } +void SpotifyService::ResetArtistsRequest() { + + artists_request_.reset(); + +} + void SpotifyService::ArtistsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -552,16 +283,6 @@ void SpotifyService::ArtistsUpdateProgressReceived(const int id, const int progr Q_EMIT ArtistsUpdateProgress(progress); } -void SpotifyService::ResetAlbumsRequest() { - - if (albums_request_) { - QObject::disconnect(&*albums_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*albums_request_, nullptr); - albums_request_.reset(); - } - -} - void SpotifyService::GetAlbums() { if (!authenticated()) { @@ -570,8 +291,7 @@ void SpotifyService::GetAlbums() { return; } - ResetAlbumsRequest(); - albums_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteAlbums, this), [](SpotifyRequest *request) { request->deleteLater(); }); + albums_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteAlbums, this)); QObject::connect(&*albums_request_, &SpotifyRequest::Results, this, &SpotifyService::AlbumsResultsReceived); QObject::connect(&*albums_request_, &SpotifyRequest::UpdateStatus, this, &SpotifyService::AlbumsUpdateStatusReceived); QObject::connect(&*albums_request_, &SpotifyRequest::ProgressSetMaximum, this, &SpotifyService::AlbumsProgressSetMaximumReceived); @@ -581,6 +301,12 @@ void SpotifyService::GetAlbums() { } +void SpotifyService::ResetAlbumsRequest() { + + albums_request_.reset(); + +} + void SpotifyService::AlbumsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -604,16 +330,6 @@ void SpotifyService::AlbumsUpdateProgressReceived(const int id, const int progre Q_EMIT AlbumsUpdateProgress(progress); } -void SpotifyService::ResetSongsRequest() { - - if (songs_request_) { - QObject::disconnect(&*songs_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*songs_request_, nullptr); - songs_request_.reset(); - } - -} - void SpotifyService::GetSongs() { if (!authenticated()) { @@ -622,8 +338,7 @@ void SpotifyService::GetSongs() { return; } - ResetSongsRequest(); - songs_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteSongs, this), [](SpotifyRequest *request) { request->deleteLater(); }); + songs_request_.reset(new SpotifyRequest(this, network_, SpotifyBaseRequest::Type::FavouriteSongs, this)); QObject::connect(&*songs_request_, &SpotifyRequest::Results, this, &SpotifyService::SongsResultsReceived); QObject::connect(&*songs_request_, &SpotifyRequest::UpdateStatus, this, &SpotifyService::SongsUpdateStatusReceived); QObject::connect(&*songs_request_, &SpotifyRequest::ProgressSetMaximum, this, &SpotifyService::SongsProgressSetMaximumReceived); @@ -633,6 +348,12 @@ void SpotifyService::GetSongs() { } +void SpotifyService::ResetSongsRequest() { + + songs_request_.reset(); + +} + void SpotifyService::SongsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -689,8 +410,7 @@ void SpotifyService::StartSearch() { } -void SpotifyService::CancelSearch() { -} +void SpotifyService::CancelSearch() {} void SpotifyService::SendSearch() { @@ -711,12 +431,11 @@ void SpotifyService::SendSearch() { return; } - search_request_.reset(new SpotifyRequest(this, network_, type, this), [](SpotifyRequest *request) { request->deleteLater(); }); - - QObject::connect(search_request_.get(), &SpotifyRequest::Results, this, &SpotifyService::SearchResultsReceived); - QObject::connect(search_request_.get(), &SpotifyRequest::UpdateStatus, this, &SpotifyService::SearchUpdateStatus); - QObject::connect(search_request_.get(), &SpotifyRequest::ProgressSetMaximum, this, &SpotifyService::SearchProgressSetMaximum); - QObject::connect(search_request_.get(), &SpotifyRequest::UpdateProgress, this, &SpotifyService::SearchUpdateProgress); + search_request_.reset(new SpotifyRequest(this, network_, type, this)); + QObject::connect(&*search_request_, &SpotifyRequest::Results, this, &SpotifyService::SearchResultsReceived); + QObject::connect(&*search_request_, &SpotifyRequest::UpdateStatus, this, &SpotifyService::SearchUpdateStatus); + QObject::connect(&*search_request_, &SpotifyRequest::ProgressSetMaximum, this, &SpotifyService::SearchProgressSetMaximum); + QObject::connect(&*search_request_, &SpotifyRequest::UpdateProgress, this, &SpotifyService::SearchUpdateProgress); search_request_->Search(search_id_, search_text_); search_request_->Process(); @@ -729,21 +448,3 @@ void SpotifyService::SearchResultsReceived(const int id, const SongMap &songs, c search_request_.reset(); } - -void SpotifyService::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) << "Spotify:" << e; - error_html += e + "
"_L1; - } - if (debug.isValid()) qLog(Debug) << debug; - - Q_EMIT LoginFailure(error_html); - Q_EMIT LoginComplete(false); - - login_errors_.clear(); - -} diff --git a/src/spotify/spotifyservice.h b/src/spotify/spotifyservice.h index 7bacac986..a1d50f4e8 100644 --- a/src/spotify/spotifyservice.h +++ b/src/spotify/spotifyservice.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2022-2024, Jonas Kvinge + * Copyright 2022-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,27 +22,16 @@ #include "config.h" -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#include -#include -#include +#include #include "includes/shared_ptr.h" #include "core/song.h" #include "streaming/streamingservice.h" -#include "streaming/streamingsearchview.h" +#include "collection/collectionmodel.h" -class QNetworkReply; +class QTimer; class TaskManager; class Database; @@ -54,7 +43,9 @@ class SpotifyStreamURLRequest; class CollectionBackend; class CollectionModel; class CollectionFilter; -class LocalRedirectServer; +class OAuthenticator; + +using SpotifyRequestPtr = QScopedPointer; class SpotifyService : public StreamingService { Q_OBJECT @@ -83,9 +74,8 @@ class SpotifyService : public StreamingService { bool fetchalbums() const { return fetchalbums_; } bool download_album_covers() const { return download_album_covers_; } - QString access_token() const { return access_token_; } - - bool authenticated() const override { return !access_token_.isEmpty(); } + bool authenticated() const override; + QByteArray authorization_header() const; SharedPtr artists_collection_backend() override { return artists_collection_backend_; } SharedPtr albums_collection_backend() override { return albums_collection_backend_; } @@ -101,7 +91,7 @@ class SpotifyService : public StreamingService { public Q_SLOTS: void Authenticate(); - void Deauthenticate(); + void ClearSession(); void GetArtists() override; void GetAlbums() override; void GetSongs() override; @@ -111,10 +101,7 @@ class SpotifyService : public StreamingService { private Q_SLOTS: void ExitReceived(); - void RedirectArrived(); - void RequestNewAccessToken() { RequestAccessToken(); } - void HandleLoginSSLErrors(const QList &ssl_errors); - void AccessTokenRequestFinished(QNetworkReply *reply); + void OAuthFinished(const bool success, const QString &error = QString()); void StartSearch(); void ArtistsResultsReceived(const int id, const SongMap &songs, const QString &error); void AlbumsResultsReceived(const int id, const SongMap &songs, const QString &error); @@ -131,16 +118,13 @@ class SpotifyService : public StreamingService { void SongsUpdateProgressReceived(const int id, const int progress); private: - using Param = QPair; - using ParamList = QList; - - void LoadSession(); - void RequestAccessToken(const QString &code = QString(), const QUrl &redirect_url = QUrl()); void SendSearch(); - void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); + private: const SharedPtr network_; + OAuthenticator *oauth_; + SharedPtr artists_collection_backend_; SharedPtr albums_collection_backend_; SharedPtr songs_collection_backend_; @@ -150,12 +134,11 @@ class SpotifyService : public StreamingService { CollectionModel *songs_collection_model_; QTimer *timer_search_delay_; - QTimer *timer_refresh_login_; - SharedPtr artists_request_; - SharedPtr albums_request_; - SharedPtr songs_request_; - SharedPtr search_request_; + SpotifyRequestPtr artists_request_; + SpotifyRequestPtr albums_request_; + SpotifyRequestPtr songs_request_; + SpotifyRequestPtr search_request_; SpotifyFavoriteRequest *favorite_request_; bool enabled_; @@ -165,11 +148,6 @@ class SpotifyService : public StreamingService { bool fetchalbums_; bool download_album_covers_; - QString access_token_; - QString refresh_token_; - quint64 expires_in_; - quint64 login_time_; - int pending_search_id_; int next_pending_search_id_; QString pending_search_text_; @@ -178,15 +156,7 @@ class SpotifyService : public StreamingService { int search_id_; QString search_text_; - QString code_verifier_; - QString code_challenge_; - - LocalRedirectServer *server_; - QStringList login_errors_; - QTimer refresh_login_timer_; - QList wait_for_exit_; - QList replies_; }; using SpotifyServicePtr = SharedPtr; diff --git a/src/streaming/streamingservice.h b/src/streaming/streamingservice.h index 06c4ebdb1..26e261511 100644 --- a/src/streaming/streamingservice.h +++ b/src/streaming/streamingservice.h @@ -59,6 +59,8 @@ class StreamingService : public QObject { virtual bool authenticated() const { return false; } virtual int Search(const QString &query, const SearchType type) { Q_UNUSED(query); Q_UNUSED(type); return 0; } virtual void CancelSearch() {} + virtual bool show_progress() const { return true; } + virtual bool enable_refresh_button() const { return true; } virtual SharedPtr artists_collection_backend() { return nullptr; } virtual SharedPtr albums_collection_backend() { return nullptr; } @@ -87,14 +89,14 @@ class StreamingService : public QObject { void RequestLogout(); void LoginWithCredentials(const QString &api_token, const QString &username, const QString &password); void LoginSuccess(); - void LoginFailure(const QString &failure_reason); - void LoginComplete(const bool success, const QString &error = QString()); + void LoginFailure(const QString &error); + void LoginFinished(const bool success, const QString &error = QString()); void TestSuccess(); - void TestFailure(const QString &failure_reason); + void TestFailure(const QString &error); void TestComplete(const bool success, const QString &error = QString()); - void Error(const QString &error); + void ShowErrorDialog(const QString &error); void Results(const SongMap &songs, const QString &error); void UpdateStatus(const QString &text); void ProgressSetMaximum(const int max); @@ -131,6 +133,7 @@ class StreamingService : public QObject { void StreamURLFailure(const uint id, const QUrl &media_url, const QString &error); void StreamURLSuccess(const uint id, const QUrl &media_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration); + void StreamURLRequestFinished(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error = QString()); void OpenSettingsDialog(const Song::Source source); diff --git a/src/streaming/streamingsongsview.cpp b/src/streaming/streamingsongsview.cpp index 9b0f47f46..668561f9f 100644 --- a/src/streaming/streamingsongsview.cpp +++ b/src/streaming/streamingsongsview.cpp @@ -55,6 +55,7 @@ StreamingSongsView::StreamingSongsView(const StreamingServicePtr service, const ui_->view->SetFilter(ui_->filter_widget); ui_->filter_widget->SetSettingsGroup(settings_group); ui_->filter_widget->Init(service_->songs_collection_model(), service_->songs_collection_filter_model()); + ui_->refresh->setVisible(service_->enable_refresh_button()); QAction *action_configure = new QAction(IconLoader::Load(u"configure"_s), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this); QObject::connect(action_configure, &QAction::triggered, this, &StreamingSongsView::Configure); @@ -66,6 +67,7 @@ StreamingSongsView::StreamingSongsView(const StreamingServicePtr service, const QObject::connect(ui_->refresh, &QPushButton::clicked, this, &StreamingSongsView::GetSongs); QObject::connect(ui_->close, &QPushButton::clicked, this, &StreamingSongsView::AbortGetSongs); QObject::connect(ui_->abort, &QPushButton::clicked, this, &StreamingSongsView::AbortGetSongs); + QObject::connect(&*service_, &StreamingService::ShowErrorDialog, this, &StreamingSongsView::ShowErrorDialog); QObject::connect(&*service_, &StreamingService::SongsResults, this, &StreamingSongsView::SongsFinished); QObject::connect(&*service_, &StreamingService::SongsUpdateStatus, ui_->status, &QLabel::setText); QObject::connect(&*service_, &StreamingService::SongsProgressSetMaximum, ui_->progressbar, &QProgressBar::setMaximum); @@ -101,11 +103,14 @@ void StreamingSongsView::GetSongs() { return; } - ui_->status->clear(); - ui_->progressbar->show(); - ui_->abort->show(); - ui_->close->hide(); - ui_->stacked->setCurrentWidget(ui_->help_page); + if (service_->show_progress()) { + ui_->status->clear(); + ui_->progressbar->show(); + ui_->abort->show(); + ui_->close->hide(); + ui_->stacked->setCurrentWidget(ui_->help_page); + } + service_->GetSongs(); } @@ -113,9 +118,12 @@ void StreamingSongsView::GetSongs() { void StreamingSongsView::AbortGetSongs() { service_->ResetSongsRequest(); - ui_->progressbar->setValue(0); - ui_->status->clear(); - ui_->stacked->setCurrentWidget(ui_->streamingcollection_page); + + if (service_->show_progress()) { + ui_->progressbar->setValue(0); + ui_->status->clear(); + ui_->stacked->setCurrentWidget(ui_->streamingcollection_page); + } } diff --git a/src/streaming/streamingsongsview.h b/src/streaming/streamingsongsview.h index f7a6304a6..fd354568e 100644 --- a/src/streaming/streamingsongsview.h +++ b/src/streaming/streamingsongsview.h @@ -58,6 +58,7 @@ class StreamingSongsView : public QWidget { void SongsFinished(const SongMap &songs, const QString &error); Q_SIGNALS: + void ShowErrorDialog(const QString &error); void OpenSettingsDialog(const Song::Source source); private: diff --git a/src/subsonic/subsonicbaserequest.cpp b/src/subsonic/subsonicbaserequest.cpp index e6cb6b410..e5f87a472 100644 --- a/src/subsonic/subsonicbaserequest.cpp +++ b/src/subsonic/subsonicbaserequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #include "config.h" #include +#include #include #include @@ -46,6 +47,7 @@ #include "constants/subsonicsettings.h" using namespace Qt::Literals::StringLiterals; +using std::make_shared; SubsonicBaseRequest::SubsonicBaseRequest(SubsonicService *service, QObject *parent) : QObject(parent), @@ -98,20 +100,20 @@ QUrl SubsonicBaseRequest::CreateUrl(const QUrl &server_url, const SubsonicSettin QNetworkReply *SubsonicBaseRequest::CreateGetRequest(const QString &ressource_name, const ParamList ¶ms_provided) const { - QUrl url = CreateUrl(server_url(), auth_method(), username(), password(), ressource_name, params_provided); - QNetworkRequest req(url); + const QUrl url = CreateUrl(server_url(), auth_method(), username(), password(), ressource_name, params_provided); + QNetworkRequest network_request(url); if (url.scheme() == "https"_L1 && !verify_certificate()) { QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration(); sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone); - req.setSslConfiguration(sslconfig); + network_request.setSslConfiguration(sslconfig); } - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2()); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2()); - QNetworkReply *reply = network_->get(req); + QNetworkReply *reply = network_->get(network_request); QObject::connect(reply, &QNetworkReply::sslErrors, this, &SubsonicBaseRequest::HandleSSLErrors); //qLog(Debug) << "Subsonic: Sending request" << url; @@ -128,103 +130,58 @@ void SubsonicBaseRequest::HandleSSLErrors(const QList &ssl_errors) { } -QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply) { +JsonBaseRequest::JsonObjectResult SubsonicBaseRequest::ParseJsonObject(QNetworkReply *reply) { - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return JsonObjectResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + + const QByteArray data = reply->readAll(); + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (!json_object.isEmpty() && json_object.contains("error"_L1) && json_object["error"_L1].isObject()) { + const QJsonObject object_error = json_object["error"_L1].toObject(); + if (!object_error.isEmpty() && object_error.contains("code"_L1) && object_error.contains("message"_L1)) { + const int code = object_error["code"_L1].toInt(); + const QString message = object_error["message"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.error_message = QStringLiteral("%s (%s)").arg(message, code); + } + } + else { + result.json_object = json_document.object(); + } } else { - - // See if there is Json data containing "error" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("error"_L1)) { - QJsonValue json_error = json_obj["error"_L1]; - if (json_error.isObject()) { - json_obj = json_error.toObject(); - if (!json_obj.isEmpty() && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) { - int code = json_obj["code"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); - error = QStringLiteral("%1 (%2)").arg(message).arg(code); - } - } - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } } - return data; - -} - -QJsonObject SubsonicBaseRequest::ExtractJsonObj(QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error(u"Reply from server missing Json data."_s, data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - Error(u"Received empty Json document."_s, data); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); - return QJsonObject(); - } - - if (!json_obj.contains("subsonic-response"_L1)) { - Error(u"Json reply is missing subsonic-response."_s, json_obj); - return QJsonObject(); - } - - QJsonValue json_response = json_obj["subsonic-response"_L1]; - if (!json_response.isObject()) { - Error(u"Json response is not an object."_s, json_response); - return QJsonObject(); - } - json_obj = json_response.toObject(); - - return json_obj; - -} - -QString SubsonicBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + "
"_L1; - } - return error_html; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + //service_->ClearSession(); + } + + return result; } diff --git a/src/subsonic/subsonicbaserequest.h b/src/subsonic/subsonicbaserequest.h index 1e63e18c0..14b23de55 100644 --- a/src/subsonic/subsonicbaserequest.h +++ b/src/subsonic/subsonicbaserequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,8 +35,9 @@ #include #include "includes/scoped_ptr.h" -#include "subsonicservice.h" #include "constants/subsonicsettings.h" +#include "core/jsonbaserequest.h" +#include "subsonicservice.h" class QNetworkAccessManager; class QNetworkReply; @@ -47,6 +48,9 @@ class SubsonicBaseRequest : public QObject { public: explicit SubsonicBaseRequest(SubsonicService *service, QObject *parent = nullptr); + using JsonObjectResult = JsonBaseRequest::JsonObjectResult; + using ErrorCode = JsonBaseRequest::ErrorCode; + protected: using Param = QPair; using ParamList = QList; @@ -56,11 +60,9 @@ class SubsonicBaseRequest : public QObject { protected: QNetworkReply *CreateGetRequest(const QString &ressource_name, const ParamList ¶ms_provided) const; - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(QByteArray &data); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - static QString ErrorsToHTML(const QStringList &errors); QUrl server_url() const { return service_->server_url(); } QString username() const { return service_->username(); } diff --git a/src/subsonic/subsonicrequest.cpp b/src/subsonic/subsonicrequest.cpp index 5fa17c3a8..86959ecdc 100644 --- a/src/subsonic/subsonicrequest.cpp +++ b/src/subsonic/subsonicrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,6 +42,7 @@ #include "core/logging.h" #include "core/song.h" #include "core/networktimeouts.h" +#include "utilities/strutils.h" #include "utilities/imageutils.h" #include "constants/timeconstants.h" #include "subsonicservice.h" @@ -144,7 +145,7 @@ void SubsonicRequest::FlushAlbumsRequests() { while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - Request request = albums_requests_queue_.dequeue(); + const Request request = albums_requests_queue_.dequeue(); ++albums_requests_active_; ParamList params = ParamList() << Param(u"type"_s, u"alphabeticalByName"_s); @@ -169,85 +170,57 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset --albums_requests_active_; - QByteArray data = GetReplyData(reply); + int albums_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, offset_requested, size_requested, &albums_received]() { AlbumsFinishCheck(offset_requested, size_requested, albums_received); }); if (finished_) return; - if (data.isEmpty()) { - AlbumsFinishCheck(offset_requested, size_requested); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(offset_requested, size_requested); + QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (json_obj.contains("error"_L1)) { - QJsonValue json_error = json_obj["error"_L1]; - if (!json_error.isObject()) { - Error(u"Json error is not an object."_s, json_obj); - AlbumsFinishCheck(offset_requested, size_requested); - return; - } - json_obj = json_error.toObject(); - if (!json_obj.isEmpty() && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) { - int code = json_obj["code"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); - Error(QStringLiteral("%1 (%2)").arg(message).arg(code)); - AlbumsFinishCheck(offset_requested, size_requested); - } - else { - Error(u"Json error object is missing code or message."_s, json_obj); - AlbumsFinishCheck(offset_requested, size_requested); - } - return; - } - - if (!json_obj.contains("albumList"_L1) && !json_obj.contains("albumList2"_L1)) { - Error(u"Json reply is missing albumList."_s, json_obj); - AlbumsFinishCheck(offset_requested, size_requested); + if (!json_object.contains("albumList"_L1) && !json_object.contains("albumList2"_L1)) { + Error(u"Json reply is missing albumList."_s, json_object); return; } QJsonValue value_albumlist; - if (json_obj.contains("albumList"_L1)) value_albumlist = json_obj["albumList"_L1]; - else if (json_obj.contains("albumList2"_L1)) value_albumlist = json_obj["albumList2"_L1]; + if (json_object.contains("albumList"_L1)) value_albumlist = json_object["albumList"_L1]; + else if (json_object.contains("albumList2"_L1)) value_albumlist = json_object["albumList2"_L1]; if (!value_albumlist.isObject()) { Error(u"Json album list is not an object."_s, value_albumlist); - AlbumsFinishCheck(offset_requested, size_requested); } - json_obj = value_albumlist.toObject(); - if (json_obj.isEmpty()) { + json_object = value_albumlist.toObject(); + if (json_object.isEmpty()) { if (offset_requested == 0) no_results_ = true; - AlbumsFinishCheck(offset_requested, size_requested); return; } - if (!json_obj.contains("album"_L1)) { - Error(u"Json album list does not contain album array."_s, json_obj); - AlbumsFinishCheck(offset_requested, size_requested); + if (!json_object.contains("album"_L1)) { + Error(u"Json album list does not contain album array."_s, json_object); } - QJsonValue json_album = json_obj["album"_L1]; + const QJsonValue json_album = json_object["album"_L1]; if (json_album.isNull()) { if (offset_requested == 0) no_results_ = true; - AlbumsFinishCheck(offset_requested, size_requested); return; } if (!json_album.isArray()) { Error(u"Json album is not an array."_s, json_album); - AlbumsFinishCheck(offset_requested, size_requested); } const QJsonArray array_albums = json_album.toArray(); if (array_albums.isEmpty()) { if (offset_requested == 0) no_results_ = true; - AlbumsFinishCheck(offset_requested, size_requested); return; } - int albums_received = 0; for (const QJsonValue &value_album : array_albums) { ++albums_received; @@ -256,27 +229,27 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset Error(u"Invalid Json reply, album is not an object."_s); continue; } - QJsonObject obj_album = value_album.toObject(); + const QJsonObject object_album = value_album.toObject(); - if (!obj_album.contains("id"_L1) || !obj_album.contains("artist"_L1)) { - Error(u"Invalid Json reply, album object in array is missing ID or artist."_s, obj_album); + if (!object_album.contains("id"_L1) || !object_album.contains("artist"_L1)) { + Error(u"Invalid Json reply, album object in array is missing ID or artist."_s, object_album); continue; } - if (!obj_album.contains("album"_L1) && !obj_album.contains("name"_L1)) { - Error(u"Invalid Json reply, album object in array is missing album or name."_s, obj_album); + if (!object_album.contains("album"_L1) && !object_album.contains("name"_L1)) { + Error(u"Invalid Json reply, album object in array is missing album or name."_s, object_album); continue; } - QString album_id = obj_album["id"_L1].toString(); + QString album_id = object_album["id"_L1].toString(); if (album_id.isEmpty()) { - album_id = QString::number(obj_album["id"_L1].toInt()); + album_id = QString::number(object_album["id"_L1].toInt()); } - QString artist = obj_album["artist"_L1].toString(); + const QString artist = object_album["artist"_L1].toString(); QString album; - if (obj_album.contains("album"_L1)) album = obj_album["album"_L1].toString(); - else if (obj_album.contains("name"_L1)) album = obj_album["name"_L1].toString(); + if (object_album.contains("album"_L1)) album = object_album["album"_L1].toString(); + else if (object_album.contains("name"_L1)) album = object_album["name"_L1].toString(); if (album_songs_requests_pending_.contains(album_id)) continue; @@ -287,8 +260,6 @@ void SubsonicRequest::AlbumsReplyReceived(QNetworkReply *reply, const int offset } - AlbumsFinishCheck(offset_requested, size_requested, albums_received); - } void SubsonicRequest::AlbumsFinishCheck(const int offset, const int size, const int albums_received) { @@ -340,14 +311,12 @@ void SubsonicRequest::AddAlbumSongsRequest(const QString &artist_id, const QStri void SubsonicRequest::FlushAlbumSongsRequests() { while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { - - Request request = album_songs_requests_queue_.dequeue(); + const Request request = album_songs_requests_queue_.dequeue(); ++album_songs_requests_active_; QNetworkReply *reply = CreateGetRequest(u"getAlbum"_s, ParamList() << Param(u"id"_s, request.album_id)); replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumSongsReplyReceived(reply, request.artist_id, request.album_id, request.album_artist); }); timeouts_->AddReply(reply); - } } @@ -364,72 +333,47 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QStrin Q_EMIT UpdateProgress(album_songs_received_); - QByteArray data = GetReplyData(reply); + const QScopeGuard finish_check = qScopeGuard([this]() { SongsFinishCheck(); }); if (finished_) return; - if (data.isEmpty()) { - SongsFinishCheck(); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (json_obj.contains("error"_L1)) { - QJsonValue json_error = json_obj["error"_L1]; - if (!json_error.isObject()) { - Error(u"Json error is not an object."_s, json_obj); - SongsFinishCheck(); - return; - } - json_obj = json_error.toObject(); - if (!json_obj.isEmpty() && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) { - int code = json_obj["code"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); - Error(QStringLiteral("%1 (%2)").arg(message).arg(code)); - SongsFinishCheck(); - } - else { - Error(u"Json error object missing code or message."_s, json_obj); - SongsFinishCheck(); - } + if (!json_object.contains("album"_L1)) { + Error(u"Json reply is missing albumList."_s, json_object); return; } - - if (!json_obj.contains("album"_L1)) { - Error(u"Json reply is missing albumList."_s, json_obj); - SongsFinishCheck(); - return; - } - QJsonValue value_album = json_obj["album"_L1]; + const QJsonValue value_album = json_object["album"_L1]; if (!value_album.isObject()) { Error(u"Json album is not an object."_s, value_album); - SongsFinishCheck(); return; } - QJsonObject obj_album = value_album.toObject(); + const QJsonObject object_album = value_album.toObject(); - if (!obj_album.contains("song"_L1)) { - Error(u"Json album object does not contain song array."_s, json_obj); - SongsFinishCheck(); + if (!object_album.contains("song"_L1)) { + Error(u"Json album object does not contain song array."_s, object_album); return; } - QJsonValue json_song = obj_album["song"_L1]; - if (!json_song.isArray()) { - Error(u"Json song is not an array."_s, obj_album); - SongsFinishCheck(); + const QJsonValue value_songs = object_album["song"_L1]; + if (!value_songs.isArray()) { + Error(u"Json song is not an array."_s, object_album); return; } - const QJsonArray array_songs = json_song.toArray(); + const QJsonArray array_songs = value_songs.toArray(); qint64 created = 0; - if (obj_album.contains("created"_L1)) { - created = QDateTime::fromString(obj_album["created"_L1].toString(), Qt::ISODate).toSecsSinceEpoch(); + if (object_album.contains("created"_L1)) { + created = QDateTime::fromString(object_album["created"_L1].toString(), Qt::ISODate).toSecsSinceEpoch(); } bool compilation = false; @@ -441,10 +385,10 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QStrin Error(u"Invalid Json reply, track is not a object."_s); continue; } - QJsonObject obj_song = value_song.toObject(); + const QJsonObject object_song = value_song.toObject(); Song song(Song::Source::Subsonic); - ParseSong(song, obj_song, artist_id, album_id, album_artist, created); + ParseSong(song, object_song, artist_id, album_id, album_artist, created); if (!song.is_valid()) continue; if (song.disc() >= 2) multidisc = true; if (song.is_compilation()) compilation = true; @@ -459,8 +403,6 @@ void SubsonicRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const QStrin songs_.insert(song.song_id(), song); } - SongsFinishCheck(); - } void SubsonicRequest::SongsFinishCheck() { @@ -485,137 +427,137 @@ void SubsonicRequest::SongsFinishCheck() { } -QString SubsonicRequest::ParseSong(Song &song, const QJsonObject &json_obj, const QString &artist_id_requested, const QString &album_id_requested, const QString &album_artist, const qint64 album_created) { +QString SubsonicRequest::ParseSong(Song &song, const QJsonObject &json_object, const QString &artist_id_requested, const QString &album_id_requested, const QString &album_artist, const qint64 album_created) { Q_UNUSED(artist_id_requested); Q_UNUSED(album_id_requested); if ( - !json_obj.contains("id"_L1) || - !json_obj.contains("title"_L1) || - !json_obj.contains("size"_L1) || - !json_obj.contains("suffix"_L1) || - !json_obj.contains("duration"_L1) || - !json_obj.contains("type"_L1) + !json_object.contains("id"_L1) || + !json_object.contains("title"_L1) || + !json_object.contains("size"_L1) || + !json_object.contains("suffix"_L1) || + !json_object.contains("duration"_L1) || + !json_object.contains("type"_L1) ) { - Error(u"Invalid Json reply, song is missing one or more values."_s, json_obj); + Error(u"Invalid Json reply, song is missing one or more values."_s, json_object); return QString(); } QString song_id; - if (json_obj["id"_L1].type() == QJsonValue::String) { - song_id = json_obj["id"_L1].toString(); + if (json_object["id"_L1].type() == QJsonValue::String) { + song_id = json_object["id"_L1].toString(); } else { - song_id = QString::number(json_obj["id"_L1].toInt()); + song_id = QString::number(json_object["id"_L1].toInt()); } QString album_id; - if (json_obj.contains("albumId"_L1)) { - if (json_obj["albumId"_L1].type() == QJsonValue::String) { - album_id = json_obj["albumId"_L1].toString(); + if (json_object.contains("albumId"_L1)) { + if (json_object["albumId"_L1].type() == QJsonValue::String) { + album_id = json_object["albumId"_L1].toString(); } else { - album_id = QString::number(json_obj["albumId"_L1].toInt()); + album_id = QString::number(json_object["albumId"_L1].toInt()); } } QString artist_id; - if (json_obj.contains("artistId"_L1)) { - if (json_obj["artistId"_L1].type() == QJsonValue::String) { - artist_id = json_obj["artistId"_L1].toString(); + if (json_object.contains("artistId"_L1)) { + if (json_object["artistId"_L1].type() == QJsonValue::String) { + artist_id = json_object["artistId"_L1].toString(); } else { - artist_id = QString::number(json_obj["artistId"_L1].toInt()); + artist_id = QString::number(json_object["artistId"_L1].toInt()); } } - QString title = json_obj["title"_L1].toString(); + QString title = json_object["title"_L1].toString(); QString album; - if (json_obj.contains("album"_L1)) { - album = json_obj["album"_L1].toString(); + if (json_object.contains("album"_L1)) { + album = json_object["album"_L1].toString(); } QString artist; - if (json_obj.contains("artist"_L1)) { - artist = json_obj["artist"_L1].toString(); + if (json_object.contains("artist"_L1)) { + artist = json_object["artist"_L1].toString(); } int size = 0; - if (json_obj["size"_L1].type() == QJsonValue::String) { - size = json_obj["size"_L1].toString().toInt(); + if (json_object["size"_L1].type() == QJsonValue::String) { + size = json_object["size"_L1].toString().toInt(); } else { - size = json_obj["size"_L1].toInt(); + size = json_object["size"_L1].toInt(); } qint64 duration = 0; - if (json_obj["duration"_L1].type() == QJsonValue::String) { - duration = json_obj["duration"_L1].toString().toInt() * kNsecPerSec; + if (json_object["duration"_L1].type() == QJsonValue::String) { + duration = json_object["duration"_L1].toString().toInt() * kNsecPerSec; } else { - duration = json_obj["duration"_L1].toInt() * kNsecPerSec; + duration = json_object["duration"_L1].toInt() * kNsecPerSec; } int bitrate = 0; - if (json_obj.contains("bitRate"_L1)) { - if (json_obj["bitRate"_L1].type() == QJsonValue::String) { - bitrate = json_obj["bitRate"_L1].toString().toInt(); + if (json_object.contains("bitRate"_L1)) { + if (json_object["bitRate"_L1].type() == QJsonValue::String) { + bitrate = json_object["bitRate"_L1].toString().toInt(); } else { - bitrate = json_obj["bitRate"_L1].toInt(); + bitrate = json_object["bitRate"_L1].toInt(); } } QString mimetype; - if (json_obj.contains("contentType"_L1)) { - mimetype = json_obj["contentType"_L1].toString(); + if (json_object.contains("contentType"_L1)) { + mimetype = json_object["contentType"_L1].toString(); } int year = 0; - if (json_obj.contains("year"_L1)) { - if (json_obj["year"_L1].type() == QJsonValue::String) { - year = json_obj["year"_L1].toString().toInt(); + if (json_object.contains("year"_L1)) { + if (json_object["year"_L1].type() == QJsonValue::String) { + year = json_object["year"_L1].toString().toInt(); } else { - year = json_obj["year"_L1].toInt(); + year = json_object["year"_L1].toInt(); } } int disc = 0; - if (json_obj.contains("discNumber"_L1)) { - if (json_obj["discNumber"_L1].type() == QJsonValue::String) { - disc = json_obj["discNumber"_L1].toString().toInt(); + if (json_object.contains("discNumber"_L1)) { + if (json_object["discNumber"_L1].type() == QJsonValue::String) { + disc = json_object["discNumber"_L1].toString().toInt(); } else { - disc = json_obj["discNumber"_L1].toInt(); + disc = json_object["discNumber"_L1].toInt(); } } int track = 0; - if (json_obj.contains("track"_L1)) { - if (json_obj["track"_L1].type() == QJsonValue::String) { - track = json_obj["track"_L1].toString().toInt(); + if (json_object.contains("track"_L1)) { + if (json_object["track"_L1].type() == QJsonValue::String) { + track = json_object["track"_L1].toString().toInt(); } else { - track = json_obj["track"_L1].toInt(); + track = json_object["track"_L1].toInt(); } } QString genre; - if (json_obj.contains("genre"_L1)) genre = json_obj["genre"_L1].toString(); + if (json_object.contains("genre"_L1)) genre = json_object["genre"_L1].toString(); QString cover_id; if (use_album_id_for_album_covers()) { cover_id = album_id; } else { - if (json_obj.contains("coverArt"_L1)) { - if (json_obj["coverArt"_L1].type() == QJsonValue::String) { - cover_id = json_obj["coverArt"_L1].toString(); + if (json_object.contains("coverArt"_L1)) { + if (json_object["coverArt"_L1].type() == QJsonValue::String) { + cover_id = json_object["coverArt"_L1].toString(); } else { - cover_id = QString::number(json_obj["coverArt"_L1].toInt()); + cover_id = QString::number(json_object["coverArt"_L1].toInt()); } } else { @@ -624,8 +566,8 @@ QString SubsonicRequest::ParseSong(Song &song, const QJsonObject &json_obj, cons } qint64 created = 0; - if (json_obj.contains("created"_L1)) { - created = QDateTime::fromString(json_obj["created"_L1].toString(), Qt::ISODate).toSecsSinceEpoch(); + if (json_object.contains("created"_L1)) { + created = QDateTime::fromString(json_object["created"_L1].toString(), Qt::ISODate).toSecsSinceEpoch(); } else { created = album_created; @@ -745,20 +687,20 @@ void SubsonicRequest::FlushAlbumCoverRequests() { while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { - AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); + const AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); ++album_covers_requests_active_; - QNetworkRequest req(request.url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2()); + QNetworkRequest network_request(request.url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2()); if (!verify_certificate()) { QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration(); sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone); - req.setSslConfiguration(sslconfig); + network_request.setSslConfiguration(sslconfig); } - QNetworkReply *reply = network_->get(req); + QNetworkReply *reply = network_->get(network_request); album_cover_replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumCoverReceived(reply, request); }); timeouts_->AddReply(reply); @@ -816,7 +758,7 @@ void SubsonicRequest::AlbumCoverReceived(QNetworkReply *reply, const AlbumCoverR return; } - QByteArray data = reply->readAll(); + const QByteArray data = reply->readAll(); if (data.isEmpty()) { Error(QStringLiteral("Received empty image data for %1").arg(request.url.toString())); if (album_covers_requests_sent_.contains(request.cover_id)) album_covers_requests_sent_.remove(request.cover_id); @@ -888,7 +830,7 @@ void SubsonicRequest::FinishCheck() { Q_EMIT Results(songs_, tr("Unknown error")); } else { - Q_EMIT Results(songs_, ErrorsToHTML(errors_)); + Q_EMIT Results(songs_, Utilities::StringListToHTML(errors_)); } } diff --git a/src/subsonic/subsonicrequest.h b/src/subsonic/subsonicrequest.h index 7d1efa982..d71bc1910 100644 --- a/src/subsonic/subsonicrequest.h +++ b/src/subsonic/subsonicrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -98,7 +98,7 @@ class SubsonicRequest : public SubsonicBaseRequest { void AddAlbumSongsRequest(const QString &artist_id, const QString &album_id, const QString &album_artist, const int offset = 0); void FlushAlbumSongsRequests(); - QString ParseSong(Song &song, const QJsonObject &json_obj, const QString &artist_id_requested = QString(), const QString &album_id_requested = QString(), const QString &album_artist = QString(), const qint64 album_created = 0); + QString ParseSong(Song &song, const QJsonObject &json_object, const QString &artist_id_requested = QString(), const QString &album_id_requested = QString(), const QString &album_artist = QString(), const qint64 album_created = 0); void GetAlbumCovers(); void AddAlbumCoverRequest(const Song &song); diff --git a/src/subsonic/subsonicscrobblerequest.cpp b/src/subsonic/subsonicscrobblerequest.cpp index edf7ff86a..7b1a3f8a7 100644 --- a/src/subsonic/subsonicscrobblerequest.cpp +++ b/src/subsonic/subsonicscrobblerequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * Copyright 2020-2021, Pascal Below * * Strawberry is free software: you can redistribute it and/or modify @@ -98,41 +98,19 @@ void SubsonicScrobbleRequest::ScrobbleReplyReceived(QNetworkReply *reply) { // "subsonic-response" is empty on success, but some keys like status, version, or type might be present. // Therefore, we can only check for errors. - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); FinishCheck(); return; } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { FinishCheck(); return; } - if (json_obj.contains("error"_L1)) { - QJsonValue json_error = json_obj["error"_L1]; - if (!json_error.isObject()) { - Error(u"Json error is not an object."_s, json_obj); - FinishCheck(); - return; - } - json_obj = json_error.toObject(); - if (!json_obj.isEmpty() && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) { - int code = json_obj["code"_L1].toInt(); - QString message = json_obj["message"_L1].toString(); - Error(QStringLiteral("%1 (%2)").arg(message).arg(code)); - FinishCheck(); - } - else { - Error(u"Json error object is missing code or message."_s, json_obj); - FinishCheck(); - return; - } - return; - } - FinishCheck(); } diff --git a/src/subsonic/subsonicscrobblerequest.h b/src/subsonic/subsonicscrobblerequest.h index d7a870cf2..1b718dafb 100644 --- a/src/subsonic/subsonicscrobblerequest.h +++ b/src/subsonic/subsonicscrobblerequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * Copyright 2020-2021, Pascal Below * * Strawberry is free software: you can redistribute it and/or modify diff --git a/src/subsonic/subsonicservice.cpp b/src/subsonic/subsonicservice.cpp index 4f1e6c37d..dc357db6b 100644 --- a/src/subsonic/subsonicservice.cpp +++ b/src/subsonic/subsonicservice.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -184,20 +184,20 @@ void SubsonicService::SendPingWithCredentials(QUrl url, const QString &username, url.setQuery(url_query); - QNetworkRequest req(url); + QNetworkRequest network_request(url); if (url.scheme() == "https"_L1 && !verify_certificate_) { QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration(); sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone); - req.setSslConfiguration(sslconfig); + network_request.setSslConfiguration(sslconfig); } - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - req.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2_); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + network_request.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2_); errors_.clear(); - QNetworkReply *reply = network_->get(req); + QNetworkReply *reply = network_->get(network_request); replies_ << reply; QObject::connect(reply, &QNetworkReply::sslErrors, this, &SubsonicService::HandlePingSSLErrors); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, url, username, password, auth_method]() { HandlePingReply(reply, url, username, password, auth_method); }); diff --git a/src/subsonic/subsonicservice.h b/src/subsonic/subsonicservice.h index 4da05dfee..6ae804279 100644 --- a/src/subsonic/subsonicservice.h +++ b/src/subsonic/subsonicservice.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,10 +38,10 @@ #include "includes/scoped_ptr.h" #include "includes/shared_ptr.h" -#include "core/song.h" -#include "collection/collectionmodel.h" -#include "streaming/streamingservice.h" #include "constants/subsonicsettings.h" +#include "core/song.h" +#include "streaming/streamingservice.h" +#include "collection/collectionmodel.h" class QNetworkReply; diff --git a/src/subsonic/subsonicurlhandler.cpp b/src/subsonic/subsonicurlhandler.cpp index 00e0382f1..41e4bec3b 100644 --- a/src/subsonic/subsonicurlhandler.cpp +++ b/src/subsonic/subsonicurlhandler.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * 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,7 +19,6 @@ #include "config.h" -#include #include #include diff --git a/src/subsonic/subsonicurlhandler.h b/src/subsonic/subsonicurlhandler.h index a78a83395..0c6af5652 100644 --- a/src/subsonic/subsonicurlhandler.h +++ b/src/subsonic/subsonicurlhandler.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2019-2021, Jonas Kvinge + * Copyright 2019-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,9 +22,6 @@ #include "config.h" -#include -#include -#include #include #include diff --git a/src/tidal/tidalbaserequest.cpp b/src/tidal/tidalbaserequest.cpp index 451c6baf1..24679eb80 100644 --- a/src/tidal/tidalbaserequest.cpp +++ b/src/tidal/tidalbaserequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,20 +19,11 @@ #include "config.h" -#include #include -#include -#include #include #include -#include -#include -#include -#include -#include -#include -#include #include +#include #include "includes/shared_ptr.h" #include "core/logging.h" @@ -43,150 +34,103 @@ using namespace Qt::Literals::StringLiterals; TidalBaseRequest::TidalBaseRequest(TidalService *service, const SharedPtr network, QObject *parent) - : QObject(parent), + : JsonBaseRequest(network, parent), service_(service), network_(network) {} +QString TidalBaseRequest::service_name() const { + + return service_->name(); + +} + +bool TidalBaseRequest::authentication_required() const { + + return true; + +} + +bool TidalBaseRequest::authenticated() const { + + return service_->authenticated(); + +} + +bool TidalBaseRequest::use_authorization_header() const { + + return true; + +} + +QByteArray TidalBaseRequest::authorization_header() const { + + return service_->authorization_header(); + +} + QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided) { const ParamList params = ParamList() << params_provided - << Param(u"countryCode"_s, country_code()); - - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!token_type().isEmpty() && !access_token().isEmpty()) { - req.setRawHeader("Authorization", token_type().toUtf8() + " " + access_token().toUtf8()); - } - - QNetworkReply *reply = network_->get(req); - QObject::connect(reply, &QNetworkReply::sslErrors, this, &TidalBaseRequest::HandleSSLErrors); - - //qLog(Debug) << "Tidal: Sending request" << url; - - return reply; + << Param(u"countryCode"_s, service_->country_code()); + return CreateGetRequest(QUrl(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + ressource_name), params); } -void TidalBaseRequest::HandleSSLErrors(const QList &ssl_errors) { +JsonBaseRequest::JsonObjectResult TidalBaseRequest::ParseJsonObject(QNetworkReply *reply) { - for (const QSslError &ssl_error : ssl_errors) { - Error(ssl_error.errorString()); + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); } -} - -QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); + JsonObjectResult result(ErrorCode::Success); + result.network_error = reply->error(); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { + result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); } - else { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + + const QByteArray data = reply->readAll(); + bool clear_session = false; + if (!data.isEmpty()) { + QJsonParseError json_parse_error; + const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error); + if (json_parse_error.error == QJsonParseError::NoError) { + const QJsonObject json_object = json_document.object(); + if (json_object.contains("status"_L1) && json_object.contains("subStatus"_L1) && json_object.contains("userMessage"_L1)) { + const int status = json_object["status"_L1].toInt(); + const int sub_status = json_object["subStatus"_L1].toInt(); + const QString user_message = json_object["userMessage"_L1].toString(); + result.error_code = ErrorCode::APIError; + result.api_error = status; + result.error_message = QStringLiteral("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); + if (status == 401 && sub_status == 6001) { + clear_session = true; + } + } + else { + result.json_object = json_document.object(); + } } else { - // See if there is Json data containing "status" and "userMessage" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - int status = 0; - int sub_status = 0; - 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("userMessage"_L1)) { - status = json_obj["status"_L1].toInt(); - sub_status = json_obj["subStatus"_L1].toInt(); - QString user_message = json_obj["userMessage"_L1].toString(); - error = QStringLiteral("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - if (error.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - if (status == 401 && sub_status == 6001) { // User does not have a valid session - service_->Logout(); - } - Error(error); + result.error_code = ErrorCode::ParseError; + result.error_message = json_parse_error.errorString(); } - return QByteArray(); } - return data; - -} - -QJsonObject TidalBaseRequest::ExtractJsonObj(const QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error(u"Reply from server missing Json data."_s, data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - Error(u"Received empty Json document."_s, data); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - Error(u"Json document is not an object."_s, json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(u"Received empty Json object."_s, json_doc); - return QJsonObject(); - } - - return json_obj; - -} - -QJsonValue TidalBaseRequest::ExtractItems(const QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj); - -} - -QJsonValue TidalBaseRequest::ExtractItems(const QJsonObject &json_obj) { - - if (!json_obj.contains("items"_L1)) { - Error(u"Json reply is missing items."_s, json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"_L1]; - return json_items; - -} - -QString TidalBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + "
"_L1; - } - return error_html; + if (result.error_code != ErrorCode::APIError) { + if (reply->error() != QNetworkReply::NoError) { + result.error_code = ErrorCode::NetworkError; + result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else if (result.http_status_code != 200) { + result.error_code = ErrorCode::HttpError; + result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError || clear_session) { + service_->ClearSession(); + } + + return result; } diff --git a/src/tidal/tidalbaserequest.h b/src/tidal/tidalbaserequest.h index ad33bc131..d64cfc8c4 100644 --- a/src/tidal/tidalbaserequest.h +++ b/src/tidal/tidalbaserequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,27 +22,18 @@ #include "config.h" -#include -#include -#include -#include -#include -#include #include #include -#include -#include -#include -#include -#include #include "includes/shared_ptr.h" +#include "core/jsonbaserequest.h" #include "tidalservice.h" class QNetworkReply; class NetworkAccessManager; +class TidalService; -class TidalBaseRequest : public QObject { +class TidalBaseRequest : public JsonBaseRequest { Q_OBJECT public: @@ -60,31 +51,14 @@ class TidalBaseRequest : public QObject { }; protected: - using Param = QPair; - using ParamList = QList; + QString service_name() const override; + bool authentication_required() const override; + bool authenticated() const override; + bool use_authorization_header() const override; + QByteArray authorization_header() const override; QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(const QByteArray &data); - QJsonValue ExtractItems(const QByteArray &data); - QJsonValue ExtractItems(const QJsonObject &json_obj); - - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - static QString ErrorsToHTML(const QStringList &errors); - - QString client_id() const { return service_->client_id(); } - quint64 user_id() const { return service_->user_id(); } - QString country_code() const { return service_->country_code(); } - QString quality() const { return service_->quality(); } - int artistssearchlimit() const { return service_->artistssearchlimit(); } - int albumssearchlimit() const { return service_->albumssearchlimit(); } - int songssearchlimit() const { return service_->songssearchlimit(); } - QString token_type() const { return service_->token_type(); } - QString access_token() const { return service_->access_token(); } - bool authenticated() const { return service_->authenticated(); } - - private Q_SLOTS: - void HandleSSLErrors(const QList &ssl_errors); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); private: TidalService *service_; diff --git a/src/tidal/tidalfavoriterequest.cpp b/src/tidal/tidalfavoriterequest.cpp index 9125a60ad..9503ca657 100644 --- a/src/tidal/tidalfavoriterequest.cpp +++ b/src/tidal/tidalfavoriterequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,9 +19,6 @@ #include "config.h" -#include -#include -#include #include #include #include @@ -46,17 +43,6 @@ TidalFavoriteRequest::TidalFavoriteRequest(TidalService *service, const SharedPt service_(service), network_(network) {} -TidalFavoriteRequest::~TidalFavoriteRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - QString TidalFavoriteRequest::FavoriteText(const FavoriteType type) { switch (type) { @@ -135,7 +121,7 @@ void TidalFavoriteRequest::AddFavorites(const FavoriteType type, const SongList void TidalFavoriteRequest::AddFavoritesRequest(const FavoriteType type, const QStringList &id_list, const SongList &songs) { - const ParamList params = ParamList() << Param(u"countryCode"_s, country_code()) + const ParamList params = ParamList() << Param(u"countryCode"_s, service_->country_code()) << Param(FavoriteMethod(type), id_list.join(u',')); QUrlQuery url_query; @@ -143,15 +129,15 @@ void TidalFavoriteRequest::AddFavoritesRequest(const FavoriteType type, const QS url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); } - QUrl url(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + "users/"_L1 + QString::number(service_->user_id()) + "/favorites/"_L1 + FavoriteText(type)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!token_type().isEmpty() && !access_token().isEmpty()) { - req.setRawHeader("Authorization", token_type().toUtf8() + " " + access_token().toUtf8()); + const QUrl url(QLatin1String(TidalService::kApiUrl) + QLatin1Char('/') + "users/"_L1 + QString::number(service_->user_id()) + "/favorites/"_L1 + FavoriteText(type)); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + if (authenticated()) { + network_request.setRawHeader("Authorization", authorization_header()); } - 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); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, type, songs]() { AddFavoritesReply(reply, type, songs); }); replies_ << reply; @@ -170,9 +156,9 @@ void TidalFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const Favorit return; } - GetReplyData(reply); - - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } @@ -246,7 +232,7 @@ void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongLi void TidalFavoriteRequest::RemoveFavoritesRequest(const FavoriteType type, const QString &id, const SongList &songs) { - const ParamList params = ParamList() << Param(u"countryCode"_s, country_code()); + const ParamList params = ParamList() << Param(u"countryCode"_s, service_->country_code()); QUrlQuery url_query; for (const Param ¶m : params) { @@ -255,13 +241,13 @@ void TidalFavoriteRequest::RemoveFavoritesRequest(const FavoriteType type, const QUrl url(QLatin1String(TidalService::kApiUrl) + "/users/"_L1 + QString::number(service_->user_id()) + "/favorites/"_L1 + FavoriteText(type) + "/"_L1 + id); url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); - if (!token_type().isEmpty() && !access_token().isEmpty()) { - req.setRawHeader("Authorization", token_type().toUtf8() + " " + access_token().toUtf8()); + QNetworkRequest network_request(url); + network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); + network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s); + if (authenticated()) { + network_request.setRawHeader("Authorization", authorization_header()); } - QNetworkReply *reply = network_->deleteResource(req); + QNetworkReply *reply = network_->deleteResource(network_request); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, type, songs]() { RemoveFavoritesReply(reply, type, songs); }); replies_ << reply; @@ -280,8 +266,9 @@ void TidalFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const Favo return; } - GetReplyData(reply); - if (reply->error() != QNetworkReply::NoError) { + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } diff --git a/src/tidal/tidalfavoriterequest.h b/src/tidal/tidalfavoriterequest.h index 966529c8f..7b39b5406 100644 --- a/src/tidal/tidalfavoriterequest.h +++ b/src/tidal/tidalfavoriterequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,7 +41,6 @@ class TidalFavoriteRequest : public TidalBaseRequest { public: explicit TidalFavoriteRequest(TidalService *service, const SharedPtr network, QObject *parent = nullptr); - ~TidalFavoriteRequest() override; private: enum class FavoriteType { @@ -85,7 +84,6 @@ class TidalFavoriteRequest : public TidalBaseRequest { TidalService *service_; const SharedPtr network_; - QList replies_; }; #endif // TIDALFAVORITEREQUEST_H diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp index 0af609297..569a1548a 100644 --- a/src/tidal/tidalrequest.cpp +++ b/src/tidal/tidalrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,7 +21,6 @@ #include -#include #include #include #include @@ -35,6 +34,7 @@ #include #include #include +#include #include "includes/shared_ptr.h" #include "core/logging.h" @@ -107,24 +107,6 @@ TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, } -TidalRequest::~TidalRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - - while (!album_cover_replies_.isEmpty()) { - QNetworkReply *reply = album_cover_replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - -} - void TidalRequest::Process() { switch (query_type_) { @@ -227,7 +209,7 @@ void TidalRequest::FlushArtistsRequests() { while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { - Request request = artists_requests_queue_.dequeue(); + const Request request = artists_requests_queue_.dequeue(); ParamList parameters; if (query_type_ == Type::SearchArtists) parameters << Param(u"query"_s, search_text_); @@ -241,7 +223,6 @@ void TidalRequest::FlushArtistsRequests() { reply = CreateRequest(u"search/artists"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { ArtistsReplyReceived(reply, request.limit, request.offset); }); ++artists_requests_active_; @@ -275,7 +256,7 @@ void TidalRequest::FlushAlbumsRequests() { while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - Request request = albums_requests_queue_.dequeue(); + const Request request = albums_requests_queue_.dequeue(); ParamList parameters; if (query_type_ == Type::SearchAlbums) parameters << Param(u"query"_s, search_text_); @@ -289,7 +270,6 @@ void TidalRequest::FlushAlbumsRequests() { reply = CreateRequest(u"search/albums"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumsReplyReceived(reply, request.limit, request.offset); }); ++albums_requests_active_; @@ -323,7 +303,7 @@ void TidalRequest::FlushSongsRequests() { while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { - Request request = songs_requests_queue_.dequeue(); + const Request request = songs_requests_queue_.dequeue(); ParamList parameters; if (query_type_ == Type::SearchSongs) parameters << Param(u"query"_s, search_text_); @@ -337,7 +317,6 @@ void TidalRequest::FlushSongsRequests() { reply = CreateRequest(u"search/tracks"_s, parameters); } if (!reply) continue; - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { SongsReplyReceived(reply, request.limit, request.offset); }); ++songs_requests_active_; @@ -395,48 +374,44 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); --artists_requests_active_; ++artists_requests_received_; if (finished_) return; - if (data.isEmpty()) { - ArtistsFinishCheck(); - return; - } + int offset = 0; + int artists_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, limit_requested, &offset, &artists_received]() { ArtistsFinishCheck(limit_requested, offset, artists_received); }); - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - ArtistsFinishCheck(); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } + const QJsonObject &json_object = json_object_result.json_object; - if (!json_obj.contains("limit"_L1) || - !json_obj.contains("offset"_L1) || - !json_obj.contains("totalNumberOfItems"_L1) || - !json_obj.contains("items"_L1)) { - Error(u"Json object missing values."_s, json_obj); - ArtistsFinishCheck(); + if (!json_object.contains("limit"_L1) || + !json_object.contains("offset"_L1) || + !json_object.contains("totalNumberOfItems"_L1) || + !json_object.contains("items"_L1)) { + Error(u"Json object missing values."_s, json_object); return; } - //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"_L1].toInt(); - int artists_total = json_obj["totalNumberOfItems"_L1].toInt(); + //int limit = json_object["limit"].toInt(); + offset = json_object["offset"_L1].toInt(); + const int artists_total = json_object["totalNumberOfItems"_L1].toInt(); if (offset_requested == 0) { artists_total_ = artists_total; } else if (artists_total != artists_total_) { Error(QStringLiteral("totalNumberOfItems returned does not match previous totalNumberOfItems! %1 != %2").arg(artists_total).arg(artists_total_)); - ArtistsFinishCheck(); return; } if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - ArtistsFinishCheck(); return; } @@ -444,19 +419,17 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re Q_EMIT UpdateProgress(query_id_, GetProgress(artists_received_, artists_total_)); } - QJsonValue value_items = ExtractItems(json_obj); - if (!value_items.isArray()) { - ArtistsFinishCheck(); + const JsonArrayResult json_array_result = GetJsonArray(json_object, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { // Empty array means no results - ArtistsFinishCheck(); return; } - int artists_received = 0; for (const QJsonValue &value_item : array_items) { ++artists_received; @@ -465,30 +438,30 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re Error(u"Invalid Json reply, item in array is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1)) { - QJsonValue json_item = obj_item["item"_L1]; + if (object_item.contains("item"_L1)) { + const QJsonValue json_item = object_item["item"_L1]; if (!json_item.isObject()) { Error(u"Invalid Json reply, item in array is not a object."_s, json_item); continue; } - obj_item = json_item.toObject(); + object_item = json_item.toObject(); } - if (!obj_item.contains("id"_L1) || !obj_item.contains("name"_L1)) { - Error(u"Invalid Json reply, item missing id or album."_s, obj_item); + if (!object_item.contains("id"_L1) || !object_item.contains("name"_L1)) { + Error(u"Invalid Json reply, item missing id or album."_s, object_item); continue; } Artist artist; - if (obj_item["id"_L1].isString()) { - artist.artist_id = obj_item["id"_L1].toString(); + if (object_item["id"_L1].isString()) { + artist.artist_id = object_item["id"_L1].toString(); } else { - artist.artist_id = QString::number(obj_item["id"_L1].toInt()); + artist.artist_id = QString::number(object_item["id"_L1].toInt()); } - artist.artist = obj_item["name"_L1].toString(); + artist.artist = object_item["name"_L1].toString(); if (artist_albums_requests_pending_.contains(artist.artist_id)) continue; @@ -501,8 +474,6 @@ void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_re if (offset_requested != 0) Q_EMIT UpdateProgress(query_id_, GetProgress(artists_received_, artists_total_)); - ArtistsFinishCheck(limit_requested, offset, artists_received); - } void TidalRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { @@ -569,7 +540,6 @@ void TidalRequest::FlushArtistAlbumsRequests() { if (request.offset > 0) parameters << Param(u"offset"_s, QString::number(request.offset)); QNetworkReply *reply = CreateRequest(QStringLiteral("artists/%1/albums").arg(request.artist.artist_id), parameters); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { ArtistAlbumsReplyReceived(reply, request.artist, request.offset); }); - replies_ << reply; ++artist_albums_requests_active_; @@ -593,52 +563,54 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - AlbumsFinishCheck(artist_requested); + int offset = 0; + int albums_total = 0; + int albums_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, artist_requested, limit_requested, &offset, &albums_total, &albums_received]() { AlbumsFinishCheck(artist_requested, limit_requested, offset, albums_total, albums_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(artist_requested); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { + Error(json_object_result.error_message); return; } - if (!json_obj.contains("limit"_L1) || - !json_obj.contains("offset"_L1) || - !json_obj.contains("totalNumberOfItems"_L1) || - !json_obj.contains("items"_L1)) { - Error(u"Json object missing values."_s, json_obj); - AlbumsFinishCheck(artist_requested); + if (!json_object.contains("limit"_L1) || + !json_object.contains("offset"_L1) || + !json_object.contains("totalNumberOfItems"_L1) || + !json_object.contains("items"_L1)) { + Error(u"Json object missing values."_s, json_object); return; } //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"_L1].toInt(); - int albums_total = json_obj["totalNumberOfItems"_L1].toInt(); + offset = json_object["offset"_L1].toInt(); + albums_total = json_object["totalNumberOfItems"_L1].toInt(); if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - AlbumsFinishCheck(artist_requested); return; } - QJsonValue value_items = ExtractItems(json_obj); - if (!value_items.isArray()) { - AlbumsFinishCheck(artist_requested); + const JsonArrayResult json_array_result = GetJsonArray(json_object, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = value_items.toArray(); + + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { - AlbumsFinishCheck(artist_requested); return; } - int albums_received = 0; for (const QJsonValue &value_item : array_items) { ++albums_received; @@ -647,79 +619,79 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req Error(u"Invalid Json reply, item in array is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1)) { - QJsonValue json_item = obj_item["item"_L1]; + if (object_item.contains("item"_L1)) { + const QJsonValue json_item = object_item["item"_L1]; if (!json_item.isObject()) { Error(u"Invalid Json reply, item in array is not a object."_s, json_item); continue; } - obj_item = json_item.toObject(); + object_item = json_item.toObject(); } Album album; - if (obj_item.contains("type"_L1)) { // This was an albums request or search - if (!obj_item.contains("id"_L1) || !obj_item.contains("title"_L1)) { - Error(u"Invalid Json reply, item is missing ID or title."_s, obj_item); + if (object_item.contains("type"_L1)) { // This was an albums request or search + if (!object_item.contains("id"_L1) || !object_item.contains("title"_L1)) { + Error(u"Invalid Json reply, item is missing ID or title."_s, object_item); continue; } - if (obj_item["id"_L1].isString()) { - album.album_id = obj_item["id"_L1].toString(); + if (object_item["id"_L1].isString()) { + album.album_id = object_item["id"_L1].toString(); } else { - album.album_id = QString::number(obj_item["id"_L1].toInt()); + album.album_id = QString::number(object_item["id"_L1].toInt()); } - album.album = obj_item["title"_L1].toString(); - if (service_->album_explicit() && obj_item.contains("explicit"_L1)) { - album.album_explicit = obj_item["explicit"_L1].toVariant().toBool(); + album.album = object_item["title"_L1].toString(); + if (service_->album_explicit() && object_item.contains("explicit"_L1)) { + album.album_explicit = object_item["explicit"_L1].toVariant().toBool(); if (album.album_explicit && !album.album.isEmpty()) { album.album.append(" (Explicit)"_L1); } } } - else if (obj_item.contains("album"_L1)) { // This was a tracks request or search - QJsonValue value_album = obj_item["album"_L1]; + else if (object_item.contains("album"_L1)) { // This was a tracks request or search + const QJsonValue value_album = object_item["album"_L1]; if (!value_album.isObject()) { Error(u"Invalid Json reply, item album is not a object."_s, value_album); continue; } - QJsonObject obj_album = value_album.toObject(); - if (!obj_album.contains("id"_L1) || !obj_album.contains("title"_L1)) { - Error(u"Invalid Json reply, item album is missing ID or title."_s, obj_album); + const QJsonObject object_album = value_album.toObject(); + if (!object_album.contains("id"_L1) || !object_album.contains("title"_L1)) { + Error(u"Invalid Json reply, item album is missing ID or title."_s, object_album); continue; } - if (obj_album["id"_L1].isString()) { - album.album_id = obj_album["id"_L1].toString(); + if (object_album["id"_L1].isString()) { + album.album_id = object_album["id"_L1].toString(); } else { - album.album_id = QString::number(obj_album["id"_L1].toInt()); + album.album_id = QString::number(object_album["id"_L1].toInt()); } - album.album = obj_album["title"_L1].toString(); - if (service_->album_explicit() && obj_album.contains("explicit"_L1)) { - album.album_explicit = obj_album["explicit"_L1].toVariant().toBool(); + album.album = object_album["title"_L1].toString(); + if (service_->album_explicit() && object_album.contains("explicit"_L1)) { + album.album_explicit = object_album["explicit"_L1].toVariant().toBool(); if (album.album_explicit && !album.album.isEmpty()) { album.album.append(" (Explicit)"_L1); } } } else { - Error(u"Invalid Json reply, item missing type or album."_s, obj_item); + Error(u"Invalid Json reply, item missing type or album."_s, object_item); continue; } if (album_songs_requests_pending_.contains(album.album_id)) continue; - if (!obj_item.contains("artist"_L1) || !obj_item.contains("title"_L1) || !obj_item.contains("audioQuality"_L1)) { - Error(u"Invalid Json reply, item missing artist, title or audioQuality."_s, obj_item); + if (!object_item.contains("artist"_L1) || !object_item.contains("title"_L1) || !object_item.contains("audioQuality"_L1)) { + Error(u"Invalid Json reply, item missing artist, title or audioQuality."_s, object_item); continue; } - QJsonValue value_artist = obj_item["artist"_L1]; + const QJsonValue value_artist = object_item["artist"_L1]; if (!value_artist.isObject()) { Error(u"Invalid Json reply, item artist is not a object."_s, value_artist); continue; } - QJsonObject obj_artist = value_artist.toObject(); + const QJsonObject obj_artist = value_artist.toObject(); if (!obj_artist.contains("id"_L1) || !obj_artist.contains("name"_L1)) { Error(u"Invalid Json reply, item artist missing id or name."_s, obj_artist); continue; @@ -751,8 +723,6 @@ void TidalRequest::AlbumsReceived(QNetworkReply *reply, const Artist &artist_req Q_EMIT UpdateProgress(query_id_, GetProgress(albums_received_, albums_total_)); } - AlbumsFinishCheck(artist_requested, limit_requested, offset, albums_total, albums_received); - } void TidalRequest::AlbumsFinishCheck(const Artist &artist, const int limit, const int offset, const int albums_total, const int albums_received) { @@ -843,7 +813,6 @@ void TidalRequest::FlushAlbumSongsRequests() { ParamList parameters; if (request.offset > 0) parameters << Param(u"offset"_s, QString::number(request.offset)); QNetworkReply *reply = CreateRequest(QStringLiteral("albums/%1/tracks").arg(request.album.album_id), parameters); - replies_ << reply; QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumSongsReplyReceived(reply, request.artist, request.album, request.offset); }); ++album_songs_requests_active_; @@ -870,76 +839,75 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const Artist &artist, con QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QByteArray data = GetReplyData(reply); + const JsonObjectResult json_object_result = ParseJsonObject(reply); if (finished_) return; - if (data.isEmpty()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested); + int songs_total = 0; + int songs_received = 0; + const QScopeGuard finish_check = qScopeGuard([this, artist, album, limit_requested, offset_requested, &songs_total, &songs_received]() { SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, songs_received); }); + + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj.contains("limit"_L1) || - !json_obj.contains("offset"_L1) || - !json_obj.contains("totalNumberOfItems"_L1) || - !json_obj.contains("items"_L1)) { - Error(u"Json object missing values."_s, json_obj); - SongsFinishCheck(artist, album, limit_requested, offset_requested); + if (!json_object.contains("limit"_L1) || + !json_object.contains("offset"_L1) || + !json_object.contains("totalNumberOfItems"_L1) || + !json_object.contains("items"_L1)) { + Error(u"Json object missing values."_s, json_object); return; } //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"_L1].toInt(); - int songs_total = json_obj["totalNumberOfItems"_L1].toInt(); + const int offset = json_object["offset"_L1].toInt(); + songs_total = json_object["totalNumberOfItems"_L1].toInt(); if (offset != offset_requested) { Error(QStringLiteral("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); return; } - QJsonValue json_value = ExtractItems(json_obj); - if (!json_value.isArray()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); + const JsonArrayResult json_array_result = GetJsonArray(json_object, u"items"_s); + if (!json_array_result.success()) { + Error(json_array_result.error_message); return; } - const QJsonArray array_items = json_value.toArray(); + const QJsonArray &array_items = json_array_result.json_array; if (array_items.isEmpty()) { - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, 0); return; } bool compilation = false; bool multidisc = false; SongList songs; - int songs_received = 0; for (const QJsonValue &value_item : array_items) { if (!value_item.isObject()) { Error(u"Invalid Json reply, track is not a object."_s); continue; } - QJsonObject obj_item = value_item.toObject(); + QJsonObject object_item = value_item.toObject(); - if (obj_item.contains("item"_L1)) { - QJsonValue item = obj_item["item"_L1]; + if (object_item.contains("item"_L1)) { + const QJsonValue item = object_item["item"_L1]; if (!item.isObject()) { Error(u"Invalid Json reply, item is not a object."_s, item); continue; } - obj_item = item.toObject(); + object_item = item.toObject(); } ++songs_received; Song song(Song::Source::Tidal); - ParseSong(song, obj_item, artist, album); + ParseSong(song, object_item, artist, album); if (!song.is_valid()) continue; if (song.disc() >= 2) multidisc = true; if (song.is_compilation()) compilation = true; @@ -957,8 +925,6 @@ void TidalRequest::SongsReceived(QNetworkReply *reply, const Artist &artist, con Q_EMIT UpdateProgress(query_id_, GetProgress(songs_received_, songs_total_)); } - SongsFinishCheck(artist, album, limit_requested, offset_requested, songs_total, songs_received); - } void TidalRequest::SongsFinishCheck(const Artist &artist, const Album &album, const int limit, const int offset, const int songs_total, const int songs_received) { @@ -1017,10 +983,10 @@ void TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti return; } - QJsonValue value_artist = json_obj["artist"_L1]; - QJsonValue value_album = json_obj["album"_L1]; - QJsonValue json_duration = json_obj["duration"_L1]; - //QJsonArray array_artists = json_obj["artists"].toArray(); + const QJsonValue value_artist = json_obj["artist"_L1]; + const QJsonValue value_album = json_obj["album"_L1]; + const QJsonValue json_duration = json_obj["duration"_L1]; + //const QJsonArray array_artists = json_obj["artists"].toArray(); QString song_id; if (json_obj["id"_L1].isString()) { @@ -1031,52 +997,52 @@ void TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti } QString title = json_obj["title"_L1].toString(); - //QString urlstr = json_obj["url"].toString(); - int track = json_obj["trackNumber"_L1].toInt(); - int disc = json_obj["volumeNumber"_L1].toInt(); - bool allow_streaming = json_obj["allowStreaming"_L1].toBool(); - bool stream_ready = json_obj["streamReady"_L1].toBool(); - QString copyright = json_obj["copyright"_L1].toString(); + //const QString urlstr = json_obj["url"].toString(); + const int track = json_obj["trackNumber"_L1].toInt(); + const int disc = json_obj["volumeNumber"_L1].toInt(); + const bool allow_streaming = json_obj["allowStreaming"_L1].toBool(); + const bool stream_ready = json_obj["streamReady"_L1].toBool(); + const QString copyright = json_obj["copyright"_L1].toString(); if (!value_artist.isObject()) { Error(u"Invalid Json reply, track artist is not a object."_s, value_artist); return; } - QJsonObject obj_artist = value_artist.toObject(); - if (!obj_artist.contains("id"_L1) || !obj_artist.contains("name"_L1)) { - Error(u"Invalid Json reply, track artist is missing id or name."_s, obj_artist); + const QJsonObject object_artist = value_artist.toObject(); + if (!object_artist.contains("id"_L1) || !object_artist.contains("name"_L1)) { + Error(u"Invalid Json reply, track artist is missing id or name."_s, object_artist); return; } QString artist_id; - if (obj_artist["id"_L1].isString()) { - artist_id = obj_artist["id"_L1].toString(); + if (object_artist["id"_L1].isString()) { + artist_id = object_artist["id"_L1].toString(); } else { - artist_id = QString::number(obj_artist["id"_L1].toInt()); + artist_id = QString::number(object_artist["id"_L1].toInt()); } - QString artist = obj_artist["name"_L1].toString(); + QString artist = object_artist["name"_L1].toString(); if (!value_album.isObject()) { Error(u"Invalid Json reply, track album is not a object."_s, value_album); return; } - QJsonObject obj_album = value_album.toObject(); - if (!obj_album.contains("id"_L1) || !obj_album.contains("title"_L1)) { - Error(u"Invalid Json reply, track album is missing ID or title."_s, obj_album); + const QJsonObject object_album = value_album.toObject(); + if (!object_album.contains("id"_L1) || !object_album.contains("title"_L1)) { + Error(u"Invalid Json reply, track album is missing ID or title."_s, object_album); return; } QString album_id; - if (obj_album["id"_L1].isString()) { - album_id = obj_album["id"_L1].toString(); + if (object_album["id"_L1].isString()) { + album_id = object_album["id"_L1].toString(); } else { - album_id = QString::number(obj_album["id"_L1].toInt()); + album_id = QString::number(object_album["id"_L1].toInt()); } if (!album.album_id.isEmpty() && album.album_id != album_id) { - Error(u"Invalid Json reply, track album id is wrong."_s, obj_album); + Error(u"Invalid Json reply, track album id is wrong."_s, object_album); return; } - QString album_title = obj_album["title"_L1].toString(); + QString album_title = object_album["title"_L1].toString(); if (album.album_explicit) album_title.append(" (Explicit)"_L1); if (!allow_streaming) { @@ -1104,8 +1070,8 @@ void TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const Arti } QUrl cover_url; - if (obj_album.contains("cover"_L1)) { - const QString cover = obj_album["cover"_L1].toString().replace(u'-', u'/'); + if (object_album.contains("cover"_L1)) { + const QString cover = object_album["cover"_L1].toString().replace(u'-', u'/'); if (!cover.isEmpty()) { cover_url.setUrl(QStringLiteral("%1/images/%2/%3.jpg").arg(QLatin1String(kResourcesUrl), cover, coversize_)); } @@ -1207,11 +1173,7 @@ void TidalRequest::FlushAlbumCoverRequests() { while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); - - QNetworkRequest req(request.url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - album_cover_replies_ << reply; + QNetworkReply *reply = CreateGetRequest(request.url); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { AlbumCoverReceived(reply, request.album_id, request.url, request.filename); }); ++album_covers_requests_active_; @@ -1222,8 +1184,8 @@ void TidalRequest::FlushAlbumCoverRequests() { void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename) { - if (album_cover_replies_.contains(reply)) { - album_cover_replies_.removeAll(reply); + if (replies_.contains(reply)) { + replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); } @@ -1237,24 +1199,23 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album if (finished_) return; + const QScopeGuard finish_check = qScopeGuard([this]() { AlbumCoverFinishCheck(); }); + Q_EMIT UpdateProgress(query_id_, GetProgress(album_covers_requests_received_, album_covers_requests_total_)); if (!album_covers_requests_sent_.contains(album_id)) { - AlbumCoverFinishCheck(); return; } if (reply->error() != QNetworkReply::NoError) { Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { Error(QStringLiteral("Received HTTP code %1 for %2.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()).arg(url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1265,7 +1226,6 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album if (!ImageUtils::SupportedImageMimeTypes().contains(mimetype, Qt::CaseInsensitive) && !ImageUtils::SupportedImageFormats().contains(mimetype, Qt::CaseInsensitive)) { Error(QStringLiteral("Unsupported mimetype for image reader %1 for %2").arg(mimetype, url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1273,7 +1233,6 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album if (data.isEmpty()) { Error(QStringLiteral("Received empty image data for %1").arg(url.toString())); if (album_covers_requests_sent_.contains(album_id)) album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); return; } @@ -1303,8 +1262,6 @@ void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album Error(QStringLiteral("Error decoding image data from %1").arg(url.toString())); } - AlbumCoverFinishCheck(); - } void TidalRequest::AlbumCoverFinishCheck() { @@ -1332,13 +1289,13 @@ void TidalRequest::FinishCheck() { artist_albums_requests_active_ <= 0 && album_songs_requests_active_ <= 0 && album_covers_requests_active_ <= 0 - ) { + ) { if (timer_flush_requests_->isActive()) { timer_flush_requests_->stop(); } finished_ = true; if (songs_.isEmpty()) { - if (errors_.isEmpty()) { + if (error_.isEmpty()) { if (IsSearch()) { Q_EMIT Results(query_id_, SongMap(), tr("No match.")); } @@ -1347,7 +1304,7 @@ void TidalRequest::FinishCheck() { } } else { - Q_EMIT Results(query_id_, SongMap(), ErrorsToHTML(errors_)); + Q_EMIT Results(query_id_, SongMap(), error_); } } else { @@ -1363,22 +1320,20 @@ int TidalRequest::GetProgress(const int count, const int total) { } -void TidalRequest::Error(const QString &error, const QVariant &debug) { +void TidalRequest::Error(const QString &error_message, const QVariant &debug_output) { - if (!error.isEmpty()) { - errors_ << error; - qLog(Error) << "Tidal:" << error; + qLog(Error) << "Tidal:" << error_message; + if (debug_output.isValid()) { + qLog(Debug) << debug_output; } - if (debug.isValid()) qLog(Debug) << debug; - - FinishCheck(); + error_ = error_message; } -void TidalRequest::Warn(const QString &error, const QVariant &debug) { +void TidalRequest::Warn(const QString &error_message, const QVariant &debug) { - qLog(Error) << "Tidal:" << error; + qLog(Warning) << "Tidal:" << error_message; if (debug.isValid()) qLog(Debug) << debug; } diff --git a/src/tidal/tidalrequest.h b/src/tidal/tidalrequest.h index 549b81741..523aee4a5 100644 --- a/src/tidal/tidalrequest.h +++ b/src/tidal/tidalrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,20 +22,15 @@ #include "config.h" -#include -#include -#include -#include -#include #include #include #include #include #include #include -#include #include #include +#include #include "includes/shared_ptr.h" #include "core/song.h" @@ -53,7 +48,6 @@ class TidalRequest : public TidalBaseRequest { public: explicit TidalRequest(TidalService *service, TidalUrlHandler *url_handler, const SharedPtr network, const Type query_type, QObject *parent); - ~TidalRequest() override; void ReloadSettings(); @@ -164,8 +158,8 @@ class TidalRequest : public TidalBaseRequest { int GetProgress(const int count, const int total); void FinishCheck(); - static void Warn(const QString &error, const QVariant &debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; + static void Warn(const QString &error_message, const QVariant &debug_output = QVariant()); + void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override; TidalService *service_; TidalUrlHandler *url_handler_; @@ -180,6 +174,7 @@ class TidalRequest : public TidalBaseRequest { QString search_text_; bool finished_; + QString error_; QQueue artists_requests_queue_; QQueue albums_requests_queue_; @@ -228,9 +223,8 @@ class TidalRequest : public TidalBaseRequest { int album_covers_requests_received_; SongMap songs_; - QStringList errors_; - QList replies_; - QList album_cover_replies_; }; +using TidalRequestPtr = QScopedPointer; + #endif // TIDALREQUEST_H diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp index cc6a1b7f6..c416cb7d2 100644 --- a/src/tidal/tidalservice.cpp +++ b/src/tidal/tidalservice.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,41 +19,22 @@ #include "config.h" -#include -#include #include -#include -#include -#include #include -#include #include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include "includes/shared_ptr.h" #include "core/logging.h" -#include "core/networkaccessmanager.h" +#include "core/settings.h" #include "core/database.h" #include "core/song.h" -#include "core/settings.h" #include "core/taskmanager.h" -#include "core/database.h" #include "core/networkaccessmanager.h" #include "core/urlhandlers.h" -#include "utilities/randutils.h" -#include "constants/timeconstants.h" +#include "core/oauthenticator.h" #include "constants/tidalsettings.h" #include "streaming/streamingsearchview.h" #include "collection/collectionbackend.h" @@ -65,11 +46,11 @@ #include "tidalrequest.h" #include "tidalfavoriterequest.h" #include "tidalstreamurlrequest.h" -#include "settings/tidalsettingspage.h" using namespace std::chrono_literals; using namespace Qt::Literals::StringLiterals; using std::make_shared; +using namespace TidalSettings; const Song::Source TidalService::kSource = Song::Source::Tidal; @@ -81,20 +62,12 @@ namespace { constexpr char kOAuthUrl[] = "https://login.tidal.com/authorize"; constexpr char kOAuthAccessTokenUrl[] = "https://login.tidal.com/oauth2/token"; constexpr char kOAuthRedirectUrl[] = "tidal://login/auth"; +constexpr char kOAuthScope[] = "r_usr w_usr"; constexpr char kArtistsSongsTable[] = "tidal_artists_songs"; constexpr char kAlbumsSongsTable[] = "tidal_albums_songs"; constexpr char kSongsTable[] = "tidal_songs"; -constexpr char kUserId[] = "user_id"; -constexpr char kCountryCode[] = "country_code"; -constexpr char kTokenType[] = "token_type"; -constexpr char kAccessToken[] = "access_token"; -constexpr char kRefreshToken[] = "refresh_token"; -constexpr char kSessionId[] = "session_id"; -constexpr char kExpiresIn[] = "expires_in"; -constexpr char kLoginTime[] = "login_time"; - } // namespace TidalService::TidalService(const SharedPtr task_manager, @@ -106,6 +79,7 @@ TidalService::TidalService(const SharedPtr task_manager, : StreamingService(Song::Source::Tidal, u"Tidal"_s, u"tidal"_s, QLatin1String(TidalSettings::kSettingsGroup), parent), network_(network), url_handler_(new TidalUrlHandler(task_manager, this)), + oauth_(new OAuthenticator(network, this)), artists_collection_backend_(nullptr), albums_collection_backend_(nullptr), songs_collection_backend_(nullptr), @@ -113,10 +87,8 @@ TidalService::TidalService(const SharedPtr task_manager, albums_collection_model_(nullptr), songs_collection_model_(nullptr), timer_search_delay_(new QTimer(this)), - timer_refresh_login_(new QTimer(this)), favorite_request_(new TidalFavoriteRequest(this, network_, this)), enabled_(false), - user_id_(0), artistssearchlimit_(1), albumssearchlimit_(1), songssearchlimit_(1), @@ -124,8 +96,6 @@ TidalService::TidalService(const SharedPtr task_manager, download_album_covers_(true), stream_url_method_(TidalSettings::StreamUrlMethod::StreamUrl), album_explicit_(false), - expires_in_(0), - login_time_(0), pending_search_id_(0), next_pending_search_id_(1), pending_search_type_(SearchType::Artists), @@ -134,6 +104,16 @@ TidalService::TidalService(const SharedPtr task_manager, url_handlers->Register(url_handler_); + oauth_->set_settings_group(QLatin1String(kSettingsGroup)); + oauth_->set_type(OAuthenticator::Type::Authorization_Code); + oauth_->set_authorize_url(QUrl(QLatin1String(kOAuthUrl))); + oauth_->set_redirect_url(QUrl(QLatin1String(kOAuthRedirectUrl))); + oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl))); + oauth_->set_scope(QLatin1String(kOAuthScope)); + oauth_->set_use_local_redirect_server(false); + oauth_->set_random_port(false); + QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &TidalService::OAuthFinished); + // Backends artists_collection_backend_ = make_shared(); @@ -158,9 +138,6 @@ TidalService::TidalService(const SharedPtr task_manager, timer_search_delay_->setSingleShot(true); QObject::connect(timer_search_delay_, &QTimer::timeout, this, &TidalService::StartSearch); - timer_refresh_login_->setSingleShot(true); - QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &TidalService::RequestNewAccessToken); - QObject::connect(this, &TidalService::AddArtists, favorite_request_, &TidalFavoriteRequest::AddArtists); QObject::connect(this, &TidalService::AddAlbums, favorite_request_, &TidalFavoriteRequest::AddAlbums); QObject::connect(this, &TidalService::AddSongs, favorite_request_, QOverload::of(&TidalFavoriteRequest::AddSongs)); @@ -179,23 +156,40 @@ TidalService::TidalService(const SharedPtr task_manager, QObject::connect(favorite_request_, &TidalFavoriteRequest::SongsRemoved, &*songs_collection_backend_, &CollectionBackend::DeleteSongs); TidalService::ReloadSettings(); - LoadSession(); + oauth_->LoadSession(); } TidalService::~TidalService() { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); + while (!stream_url_requests_.isEmpty()) { + TidalStreamURLRequestPtr stream_url_request = stream_url_requests_.take(stream_url_requests_.firstKey()); + QObject::disconnect(&*stream_url_request, nullptr, this, nullptr); } - while (!stream_url_requests_.isEmpty()) { - SharedPtr stream_url_req = stream_url_requests_.take(stream_url_requests_.firstKey()); - QObject::disconnect(&*stream_url_req, nullptr, this, nullptr); - } +} + +bool TidalService::authenticated() const { + + return oauth_->authenticated(); + +} + +QByteArray TidalService::authorization_header() const { + + return oauth_->authorization_header(); + +} + +QString TidalService::country_code() const { + + return oauth_->country_code(); + +} + +quint64 TidalService::user_id() const { + + return oauth_->user_id(); } @@ -223,41 +217,10 @@ void TidalService::ExitReceived() { } -void TidalService::LoadSession() { - - Settings s; - s.beginGroup(TidalSettings::kSettingsGroup); - user_id_ = s.value(kUserId).toInt(); - country_code_ = s.value(kCountryCode, u"US"_s).toString(); - token_type_ = s.value(kTokenType).toString(); - access_token_ = s.value(kAccessToken).toString(); - refresh_token_ = s.value(kRefreshToken).toString(); - expires_in_ = s.value(kExpiresIn).toLongLong(); - login_time_ = s.value(kLoginTime).toLongLong(); - s.endGroup(); - - if (token_type_.isEmpty()) { - token_type_ = "Bearer"_L1; - } - - if (!refresh_token_.isEmpty()) { - qint64 time = static_cast(expires_in_) - (QDateTime::currentSecsSinceEpoch() - static_cast(login_time_)); - if (time <= 0) { - timer_refresh_login_->setInterval(200ms); - } - else { - timer_refresh_login_->setInterval(static_cast(time * kMsecPerSec)); - } - timer_refresh_login_->start(); - } - -} - void TidalService::ReloadSettings() { Settings s; s.beginGroup(TidalSettings::kSettingsGroup); - enabled_ = s.value(TidalSettings::kEnabled, false).toBool(); client_id_ = s.value(TidalSettings::kClientId).toString(); quality_ = s.value(TidalSettings::kQuality, u"LOSSLESS"_s).toString(); @@ -270,272 +233,45 @@ void TidalService::ReloadSettings() { download_album_covers_ = s.value(TidalSettings::kDownloadAlbumCovers, true).toBool(); stream_url_method_ = static_cast(s.value(TidalSettings::kStreamUrl, static_cast(TidalSettings::StreamUrlMethod::StreamUrl)).toInt()); album_explicit_ = s.value(TidalSettings::kAlbumExplicit).toBool(); - s.endGroup(); + oauth_->set_client_id(client_id_); timer_search_delay_->setInterval(static_cast(search_delay)); } void TidalService::StartAuthorization(const QString &client_id) { - client_id_ = client_id; - code_verifier_ = Utilities::CryptographicRandomString(44); - code_challenge_ = QString::fromLatin1(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding)); + oauth_->set_client_id(client_id); + oauth_->Authenticate(); - if (code_challenge_.lastIndexOf(u'=') == code_challenge_.length() - 1) { - code_challenge_.chop(1); +} + +void TidalService::OAuthFinished(const bool success, const QString &error) { + + if (success) { + qLog(Debug) << "Tidal: Login successful" << "user id" << user_id(); + Q_EMIT LoginFinished(true); + Q_EMIT LoginSuccess(); } - - const ParamList params = ParamList() << Param(u"response_type"_s, u"code"_s) - << Param(u"code_challenge"_s, code_challenge_) - << Param(u"code_challenge_method"_s, u"S256"_s) - << Param(u"redirect_uri"_s, QLatin1String(kOAuthRedirectUrl)) - << Param(u"client_id"_s, client_id_) - << Param(u"scope"_s, u"r_usr w_usr"_s); - - QUrlQuery url_query; - for (const Param ¶m : params) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); + else { + Q_EMIT LoginFailure(error); + Q_EMIT LoginFinished(false); } - QUrl url = QUrl(QString::fromLatin1(kOAuthUrl)); - url.setQuery(url_query); - QDesktopServices::openUrl(url); - } void TidalService::AuthorizationUrlReceived(const QUrl &url) { qLog(Debug) << "Tidal: Authorization URL Received" << url; - QUrlQuery url_query(url); - - if (url_query.hasQueryItem(u"token_type"_s) && url_query.hasQueryItem(u"expires_in"_s) && url_query.hasQueryItem(u"access_token"_s)) { - - access_token_ = url_query.queryItemValue(u"access_token"_s); - if (url_query.hasQueryItem(u"refresh_token"_s)) { - refresh_token_ = url_query.queryItemValue(u"refresh_token"_s); - } - expires_in_ = url_query.queryItemValue(u"expires_in"_s).toInt(); - login_time_ = QDateTime::currentSecsSinceEpoch(); - - Settings s; - s.beginGroup(TidalSettings::kSettingsGroup); - s.setValue(kTokenType, token_type_); - s.setValue(kAccessToken, access_token_); - s.setValue(kRefreshToken, refresh_token_); - s.setValue(kExpiresIn, expires_in_); - s.setValue(kLoginTime, login_time_); - s.remove(kSessionId); - s.endGroup(); - - Q_EMIT LoginComplete(true); - Q_EMIT LoginSuccess(); - } - - else if (url_query.hasQueryItem(u"code"_s) && url_query.hasQueryItem(u"state"_s)) { - - QString code = url_query.queryItemValue(u"code"_s); - - RequestAccessToken(code); - - } - - else { - LoginError(tr("Reply from Tidal is missing query items.")); - return; - } + oauth_->ExternalAuthorizationUrlReceived(url); } -void TidalService::RequestAccessToken(const QString &code) { +void TidalService::ClearSession() { - timer_refresh_login_->stop(); - - ParamList params = ParamList() << Param(u"client_id"_s, client_id_); - - if (!code.isEmpty()) { - params << Param(u"grant_type"_s, u"authorization_code"_s); - params << Param(u"code"_s, code); - params << Param(u"code_verifier"_s, code_verifier_); - params << Param(u"redirect_uri"_s, QLatin1String(kOAuthRedirectUrl)); - params << Param(u"scope"_s, u"r_usr w_usr"_s); - } - else if (!refresh_token_.isEmpty() && enabled_) { - params << Param(u"grant_type"_s, u"refresh_token"_s); - params << Param(u"refresh_token"_s, refresh_token_); - } - else { - return; - } - - QUrlQuery url_query; - for (const Param ¶m : std::as_const(params)) { - url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second))); - } - - QUrl url(QString::fromLatin1(kOAuthAccessTokenUrl)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - const QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - - login_errors_.clear(); - QNetworkReply *reply = network_->post(req, query); - replies_ << reply; - QObject::connect(reply, &QNetworkReply::sslErrors, this, &TidalService::HandleLoginSSLErrors); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AccessTokenRequestFinished(reply); }); - -} - -void TidalService::HandleLoginSSLErrors(const QList &ssl_errors) { - - for (const QSslError &ssl_error : ssl_errors) { - login_errors_ += ssl_error.errorString(); - } - -} - -void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - LoginError(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - return; - } - else { - // See if there is Json data containing "status" and "userMessage" 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("userMessage"_L1)) { - int status = json_obj["status"_L1].toInt(); - int sub_status = json_obj["subStatus"_L1].toInt(); - QString user_message = json_obj["userMessage"_L1].toString(); - login_errors_ << QStringLiteral("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - 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; - } - } - - const QByteArray data = reply->readAll(); - QJsonParseError json_error; - QJsonDocument json_doc = 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()) { - 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); - return; - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - LoginError(u"Authentication reply from server has empty Json object."_s, json_doc); - return; - } - - if (!json_obj.contains("token_type"_L1) || !json_obj.contains("access_token"_L1) || !json_obj.contains("expires_in"_L1)) { - LoginError(u"Authentication reply from server is missing token_type, access_token or expires_in"_s, json_obj); - return; - } - - token_type_ = json_obj["token_type"_L1].toString(); - access_token_ = json_obj["access_token"_L1].toString(); - refresh_token_ = json_obj["refresh_token"_L1].toString(); - expires_in_ = json_obj["expires_in"_L1].toInt(); - login_time_ = QDateTime::currentSecsSinceEpoch(); - - if (json_obj.contains("user"_L1) && json_obj["user"_L1].isObject()) { - QJsonObject obj_user = json_obj["user"_L1].toObject(); - if (obj_user.contains("countryCode"_L1) && obj_user.contains("userId"_L1)) { - country_code_ = obj_user["countryCode"_L1].toString(); - user_id_ = obj_user["userId"_L1].toInt(); - } - } - - Settings s; - s.beginGroup(TidalSettings::kSettingsGroup); - s.setValue(kTokenType, token_type_); - s.setValue(kAccessToken, access_token_); - s.setValue(kRefreshToken, refresh_token_); - s.setValue(kExpiresIn, expires_in_); - s.setValue(kLoginTime, login_time_); - s.setValue(kCountryCode, country_code_); - s.setValue(kUserId, user_id_); - s.remove(kSessionId); - s.endGroup(); - - if (expires_in_ > 0) { - timer_refresh_login_->setInterval(static_cast(expires_in_ * kMsecPerSec)); - timer_refresh_login_->start(); - } - - qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_; - - Q_EMIT LoginComplete(true); - Q_EMIT LoginSuccess(); - -} - -void TidalService::Logout() { - - user_id_ = 0; - country_code_.clear(); - access_token_.clear(); - refresh_token_.clear(); - expires_in_ = 0; - login_time_ = 0; - - Settings s; - s.beginGroup(TidalSettings::kSettingsGroup); - s.remove(kUserId); - s.remove(kCountryCode); - s.remove(kTokenType); - s.remove(kAccessToken); - s.remove(kRefreshToken); - s.remove(kSessionId); - s.remove(kExpiresIn); - s.remove(kLoginTime); - s.endGroup(); - - timer_refresh_login_->stop(); - -} - -void TidalService::ResetArtistsRequest() { - - if (artists_request_) { - QObject::disconnect(&*artists_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*artists_request_, nullptr); - artists_request_.reset(); - } + oauth_->ClearSession(); } @@ -547,8 +283,7 @@ void TidalService::GetArtists() { return; } - ResetArtistsRequest(); - artists_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteArtists, this), [](TidalRequest *request) { request->deleteLater(); }); + artists_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteArtists, this)); QObject::connect(&*artists_request_, &TidalRequest::Results, this, &TidalService::ArtistsResultsReceived); QObject::connect(&*artists_request_, &TidalRequest::UpdateStatus, this, &TidalService::ArtistsUpdateStatusReceived); QObject::connect(&*artists_request_, &TidalRequest::UpdateProgress, this, &TidalService::ArtistsUpdateProgressReceived); @@ -557,6 +292,13 @@ void TidalService::GetArtists() { } + +void TidalService::ResetArtistsRequest() { + + artists_request_.reset(); + +} + void TidalService::ArtistsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -575,16 +317,6 @@ void TidalService::ArtistsUpdateProgressReceived(const int id, const int progres Q_EMIT ArtistsUpdateProgress(progress); } -void TidalService::ResetAlbumsRequest() { - - if (albums_request_) { - QObject::disconnect(&*albums_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*albums_request_, nullptr); - albums_request_.reset(); - } - -} - void TidalService::GetAlbums() { if (!authenticated()) { @@ -593,8 +325,7 @@ void TidalService::GetAlbums() { return; } - ResetAlbumsRequest(); - albums_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteAlbums, this), [](TidalRequest *request) { request->deleteLater(); }); + albums_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteAlbums, this)); QObject::connect(&*albums_request_, &TidalRequest::Results, this, &TidalService::AlbumsResultsReceived); QObject::connect(&*albums_request_, &TidalRequest::UpdateStatus, this, &TidalService::AlbumsUpdateStatusReceived); QObject::connect(&*albums_request_, &TidalRequest::UpdateProgress, this, &TidalService::AlbumsUpdateProgressReceived); @@ -603,6 +334,12 @@ void TidalService::GetAlbums() { } +void TidalService::ResetAlbumsRequest() { + + albums_request_.reset(); + +} + void TidalService::AlbumsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -621,16 +358,6 @@ void TidalService::AlbumsUpdateProgressReceived(const int id, const int progress Q_EMIT AlbumsUpdateProgress(progress); } -void TidalService::ResetSongsRequest() { - - if (songs_request_) { - QObject::disconnect(&*songs_request_, nullptr, this, nullptr); - QObject::disconnect(this, nullptr, &*songs_request_, nullptr); - songs_request_.reset(); - } - -} - void TidalService::GetSongs() { if (!authenticated()) { @@ -639,8 +366,7 @@ void TidalService::GetSongs() { return; } - ResetSongsRequest(); - songs_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteSongs, this), [](TidalRequest *request) { request->deleteLater(); }); + songs_request_.reset(new TidalRequest(this, url_handler_, network_, TidalBaseRequest::Type::FavouriteSongs, this)); QObject::connect(&*songs_request_, &TidalRequest::Results, this, &TidalService::SongsResultsReceived); QObject::connect(&*songs_request_, &TidalRequest::UpdateStatus, this, &TidalService::SongsUpdateStatusReceived); QObject::connect(&*songs_request_, &TidalRequest::UpdateProgress, this, &TidalService::SongsUpdateProgressReceived); @@ -649,6 +375,12 @@ void TidalService::GetSongs() { } +void TidalService::ResetSongsRequest() { + + songs_request_.reset(); + +} + void TidalService::SongsResultsReceived(const int id, const SongMap &songs, const QString &error) { Q_UNUSED(id); @@ -721,8 +453,7 @@ void TidalService::SendSearch() { return; } - search_request_.reset(new TidalRequest(this, url_handler_, network_, query_type, this), [](TidalRequest *request) { request->deleteLater(); }); - + search_request_.reset(new TidalRequest(this, url_handler_, network_, query_type, this)); QObject::connect(&*search_request_, &TidalRequest::Results, this, &TidalService::SearchResultsReceived); QObject::connect(&*search_request_, &TidalRequest::UpdateStatus, this, &TidalService::SearchUpdateStatus); QObject::connect(&*search_request_, &TidalRequest::UpdateProgress, this, &TidalService::SearchUpdateProgress); @@ -748,14 +479,11 @@ uint TidalService::GetStreamURL(const QUrl &url, QString &error) { uint id = 0; while (id == 0) id = ++next_stream_url_request_id_; - SharedPtr stream_url_req; - stream_url_req.reset(new TidalStreamURLRequest(this, network_, url, id), [](TidalStreamURLRequest *request) { request->deleteLater(); }); - stream_url_requests_.insert(id, stream_url_req); - - QObject::connect(&*stream_url_req, &TidalStreamURLRequest::StreamURLFailure, this, &TidalService::HandleStreamURLFailure); - QObject::connect(&*stream_url_req, &TidalStreamURLRequest::StreamURLSuccess, this, &TidalService::HandleStreamURLSuccess); - - stream_url_req->Process(); + TidalStreamURLRequestPtr stream_url_request = TidalStreamURLRequestPtr(new TidalStreamURLRequest(this, network_, url, id), &QObject::deleteLater); + stream_url_requests_.insert(id, stream_url_request); + QObject::connect(&*stream_url_request, &TidalStreamURLRequest::StreamURLFailure, this, &TidalService::HandleStreamURLFailure); + QObject::connect(&*stream_url_request, &TidalStreamURLRequest::StreamURLSuccess, this, &TidalService::HandleStreamURLSuccess); + stream_url_request->Process(); return id; @@ -778,21 +506,3 @@ void TidalService::HandleStreamURLSuccess(const uint id, const QUrl &media_url, Q_EMIT StreamURLSuccess(id, media_url, stream_url, filetype, samplerate, bit_depth, duration); } - -void TidalService::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) << "Tidal:" << e; - error_html += e + "
"_L1; - } - if (debug.isValid()) qLog(Debug) << debug; - - Q_EMIT LoginFailure(error_html); - Q_EMIT LoginComplete(false, error_html); - - login_errors_.clear(); - -} diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h index a1dadf85d..fcef59e57 100644 --- a/src/tidal/tidalservice.h +++ b/src/tidal/tidalservice.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,27 +22,20 @@ #include "config.h" -#include -#include -#include -#include #include #include -#include #include #include -#include #include -#include -#include +#include +#include #include "includes/shared_ptr.h" #include "core/song.h" #include "streaming/streamingservice.h" -#include "streaming/streamingsearchview.h" #include "constants/tidalsettings.h" +#include "collection/collectionmodel.h" -class QNetworkReply; class QTimer; class TaskManager; @@ -57,6 +50,10 @@ class TidalStreamURLRequest; class CollectionBackend; class CollectionModel; class CollectionFilter; +class OAuthenticator; + +using TidalRequestPtr = QScopedPointer; +using TidalStreamURLRequestPtr = QSharedPointer; class TidalService : public StreamingService { Q_OBJECT @@ -78,13 +75,11 @@ class TidalService : public StreamingService { void Exit() override; void ReloadSettings() override; - void Logout(); + void ClearSession(); int Search(const QString &text, const SearchType type) override; void CancelSearch() override; QString client_id() const { return client_id_; } - quint64 user_id() const { return user_id_; } - QString country_code() const { return country_code_; } QString quality() const { return quality_; } int artistssearchlimit() const { return artistssearchlimit_; } int albumssearchlimit() const { return albumssearchlimit_; } @@ -95,10 +90,10 @@ class TidalService : public StreamingService { TidalSettings::StreamUrlMethod stream_url_method() const { return stream_url_method_; } bool album_explicit() const { return album_explicit_; } - QString token_type() const { return token_type_; } - QString access_token() const { return access_token_; } - - bool authenticated() const override { return !token_type_.isEmpty() && !access_token_.isEmpty(); } + bool authenticated() const override; + QByteArray authorization_header() const; + QString country_code() const; + quint64 user_id() const; uint GetStreamURL(const QUrl &url, QString &error); @@ -116,19 +111,17 @@ class TidalService : public StreamingService { public Q_SLOTS: void StartAuthorization(const QString &client_id); + void AuthorizationUrlReceived(const QUrl &url); void GetArtists() override; void GetAlbums() override; void GetSongs() override; void ResetArtistsRequest() override; void ResetAlbumsRequest() override; void ResetSongsRequest() override; - void AuthorizationUrlReceived(const QUrl &url); private Q_SLOTS: void ExitReceived(); - void RequestNewAccessToken() { RequestAccessToken(); } - void HandleLoginSSLErrors(const QList &ssl_errors); - void AccessTokenRequestFinished(QNetworkReply *reply); + void OAuthFinished(const bool success, const QString &error); void StartSearch(); void ArtistsResultsReceived(const int id, const SongMap &songs, const QString &error); void AlbumsResultsReceived(const int id, const SongMap &songs, const QString &error); @@ -144,16 +137,12 @@ class TidalService : public StreamingService { void HandleStreamURLSuccess(const uint id, const QUrl &media_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration); private: - using Param = QPair; - using ParamList = QList; - - void LoadSession(); - void RequestAccessToken(const QString &code = QString()); void SendSearch(); - void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); + private: const SharedPtr network_; TidalUrlHandler *url_handler_; + OAuthenticator *oauth_; SharedPtr artists_collection_backend_; SharedPtr albums_collection_backend_; @@ -164,18 +153,15 @@ class TidalService : public StreamingService { CollectionModel *songs_collection_model_; QTimer *timer_search_delay_; - QTimer *timer_refresh_login_; - SharedPtr artists_request_; - SharedPtr albums_request_; - SharedPtr songs_request_; - SharedPtr search_request_; + TidalRequestPtr artists_request_; + TidalRequestPtr albums_request_; + TidalRequestPtr songs_request_; + TidalRequestPtr search_request_; TidalFavoriteRequest *favorite_request_; bool enabled_; QString client_id_; - quint64 user_id_; - QString country_code_; QString quality_; int artistssearchlimit_; int albumssearchlimit_; @@ -186,12 +172,6 @@ class TidalService : public StreamingService { TidalSettings::StreamUrlMethod stream_url_method_; bool album_explicit_; - QString token_type_; - QString access_token_; - QString refresh_token_; - quint64 expires_in_; - quint64 login_time_; - int pending_search_id_; int next_pending_search_id_; QString pending_search_text_; @@ -200,16 +180,10 @@ class TidalService : public StreamingService { int search_id_; QString search_text_; - QString code_verifier_; - QString code_challenge_; - uint next_stream_url_request_id_; - QMap> stream_url_requests_; - - QStringList login_errors_; + QMap stream_url_requests_; QList wait_for_exit_; - QList replies_; }; using TidalServicePtr = SharedPtr; diff --git a/src/tidal/tidalstreamurlrequest.cpp b/src/tidal/tidalstreamurlrequest.cpp index e71dae387..7590dfca1 100644 --- a/src/tidal/tidalstreamurlrequest.cpp +++ b/src/tidal/tidalstreamurlrequest.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -62,6 +62,29 @@ TidalStreamURLRequest::~TidalStreamURLRequest() { } +bool TidalStreamURLRequest::oauth() const { + + return service_->oauth(); + +} +TidalSettings::StreamUrlMethod TidalStreamURLRequest::stream_url_method() const { + + return service_->stream_url_method(); + +} + +QUrl TidalStreamURLRequest::media_url() const { + + return media_url_; + +} + +int TidalStreamURLRequest::song_id() const { + + return song_id_; + +} + void TidalStreamURLRequest::Process() { if (!authenticated()) { @@ -96,12 +119,12 @@ void TidalStreamURLRequest::GetStreamURL() { switch (stream_url_method()) { case TidalSettings::StreamUrlMethod::StreamUrl: - params << Param(u"soundQuality"_s, quality()); + params << Param(u"soundQuality"_s, service_->quality()); reply_ = CreateRequest(QStringLiteral("tracks/%1/streamUrl").arg(song_id_), params); QObject::connect(reply_, &QNetworkReply::finished, this, &TidalStreamURLRequest::StreamURLReceived); break; case TidalSettings::StreamUrlMethod::UrlPostPaywall: - params << Param(u"audioquality"_s, quality()); + params << Param(u"audioquality"_s, service_->quality()); params << Param(u"playbackmode"_s, u"STREAM"_s); params << Param(u"assetpresentation"_s, u"FULL"_s); params << Param(u"urlusagemode"_s, u"STREAM"_s); @@ -109,7 +132,7 @@ void TidalStreamURLRequest::GetStreamURL() { QObject::connect(reply_, &QNetworkReply::finished, this, &TidalStreamURLRequest::StreamURLReceived); break; case TidalSettings::StreamUrlMethod::PlaybackInfoPostPaywall: - params << Param(u"audioquality"_s, quality()); + params << Param(u"audioquality"_s, service_->quality()); params << Param(u"playbackmode"_s, u"STREAM"_s); params << Param(u"assetpresentation"_s, u"FULL"_s); reply_ = CreateRequest(QStringLiteral("tracks/%1/playbackinfopostpaywall").arg(song_id_), params); @@ -123,39 +146,36 @@ void TidalStreamURLRequest::StreamURLReceived() { if (!reply_) return; - QByteArray data = GetReplyData(reply_); + Q_ASSERT(replies_.contains(reply_)); + replies_.removeAll(reply_); + + const JsonObjectResult json_object_result = ParseJsonObject(reply_); QObject::disconnect(reply_, nullptr, this, nullptr); reply_->deleteLater(); reply_ = nullptr; - if (data.isEmpty()) { - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + if (!json_object_result.success()) { + Q_EMIT StreamURLFailure(id_, media_url_, json_object_result.error_message); return; } + const QJsonObject &json_object = json_object_result.json_object; - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + if (!json_object.contains("trackId"_L1)) { + Q_EMIT StreamURLFailure(id_, media_url_, u"Invalid Json reply, stream missing trackId."_s); return; } - - if (!json_obj.contains("trackId"_L1)) { - Error(u"Invalid Json reply, stream missing trackId."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); - return; - } - int track_id = json_obj["trackId"_L1].toInt(); + const int track_id = json_object["trackId"_L1].toInt(); if (track_id != song_id_) { qLog(Debug) << "Tidal returned track ID" << track_id << "for" << media_url_; } Song::FileType filetype(Song::FileType::Stream); - if (json_obj.contains("codec"_L1) || json_obj.contains("codecs"_L1)) { + if (json_object.contains("codec"_L1) || json_object.contains("codecs"_L1)) { QString codec; - if (json_obj.contains("codec"_L1)) codec = json_obj["codec"_L1].toString().toLower(); - if (json_obj.contains("codecs"_L1)) codec = json_obj["codecs"_L1].toString().toLower(); + if (json_object.contains("codec"_L1)) codec = json_object["codec"_L1].toString().toLower(); + if (json_object.contains("codecs"_L1)) codec = json_object["codecs"_L1].toString().toLower(); filetype = Song::FiletypeByExtension(codec); if (filetype == Song::FileType::Unknown) { qLog(Debug) << "Tidal: Unknown codec" << codec; @@ -165,10 +185,10 @@ void TidalStreamURLRequest::StreamURLReceived() { QList urls; - if (json_obj.contains("manifest"_L1)) { + if (json_object.contains("manifest"_L1)) { - QString manifest(json_obj["manifest"_L1].toString()); - QByteArray data_manifest = QByteArray::fromBase64(manifest.toUtf8()); + const QString manifest(json_object["manifest"_L1].toString()); + const QByteArray data_manifest = QByteArray::fromBase64(manifest.toUtf8()); QXmlStreamReader xml_reader(data_manifest); if (xml_reader.readNextStartElement()) { @@ -180,29 +200,28 @@ void TidalStreamURLRequest::StreamURLReceived() { else { - json_obj = ExtractJsonObj(data_manifest); - if (json_obj.isEmpty()) { - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + const JsonObjectResult json_object_result_manifest = GetJsonObject(data_manifest); + if (!json_object_result_manifest.success()) { + Q_EMIT StreamURLFailure(id_, media_url_, json_object_result_manifest.error_message); return; } + const QJsonObject &object_manifest = json_object_result_manifest.json_object; - if (json_obj.contains("encryptionType"_L1) && json_obj.contains("keyId"_L1)) { - QString encryption_type = json_obj["encryptionType"_L1].toString(); - QString key_id = json_obj["keyId"_L1].toString(); + if (object_manifest.contains("encryptionType"_L1) && object_manifest.contains("keyId"_L1)) { + QString encryption_type = object_manifest["encryptionType"_L1].toString(); + QString key_id = object_manifest["keyId"_L1].toString(); if (!encryption_type.isEmpty() && !key_id.isEmpty()) { - Error(tr("Received URL with %1 encrypted stream from Tidal. Strawberry does not currently support encrypted streams.").arg(encryption_type)); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, tr("Received URL with %1 encrypted stream from Tidal. Strawberry does not currently support encrypted streams.").arg(encryption_type)); return; } } - if (!json_obj.contains("mimeType"_L1)) { - Error(u"Invalid Json reply, stream url reply manifest is missing mimeType."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + if (!object_manifest.contains("mimeType"_L1)) { + Q_EMIT StreamURLFailure(id_, media_url_, u"Invalid Json reply, stream url reply manifest is missing mimeType."_s); return; } - QString mimetype = json_obj["mimeType"_L1].toString(); + const QString mimetype = object_manifest["mimeType"_L1].toString(); QMimeDatabase mimedb; const QStringList suffixes = mimedb.mimeTypeForName(mimetype).suffixes(); for (const QString &suffix : suffixes) { @@ -217,11 +236,10 @@ void TidalStreamURLRequest::StreamURLReceived() { } - if (json_obj.contains("urls"_L1)) { - QJsonValue json_urls = json_obj["urls"_L1]; + if (json_object.contains("urls"_L1)) { + const QJsonValue json_urls = json_object["urls"_L1]; if (!json_urls.isArray()) { - Error(u"Invalid Json reply, urls is not an array."_s, json_urls); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, u"Invalid Json reply, urls is not an array."_s); return; } const QJsonArray json_array_urls = json_urls.toArray(); @@ -230,8 +248,8 @@ void TidalStreamURLRequest::StreamURLReceived() { urls << QUrl(value.toString()); } } - else if (json_obj.contains("url"_L1)) { - QUrl new_url(json_obj["url"_L1].toString()); + else if (json_object.contains("url"_L1)) { + const QUrl new_url(json_object["url"_L1].toString()); urls << new_url; if (filetype == Song::FileType::Stream) { // Guess filetype by filename extension in URL. @@ -240,42 +258,28 @@ void TidalStreamURLRequest::StreamURLReceived() { } } - if (json_obj.contains("encryptionKey"_L1)) { - QString encryption_key = json_obj["encryptionKey"_L1].toString(); + if (json_object.contains("encryptionKey"_L1)) { + const QString encryption_key = json_object["encryptionKey"_L1].toString(); if (!encryption_key.isEmpty()) { - Error(tr("Received URL with encrypted stream from Tidal. Strawberry does not currently support encrypted streams.")); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, tr("Received URL with encrypted stream from Tidal. Strawberry does not currently support encrypted streams.")); return; } } - if (json_obj.contains("securityType"_L1) && json_obj.contains("securityToken"_L1)) { - QString security_type = json_obj["securityType"_L1].toString(); - QString security_token = json_obj["securityToken"_L1].toString(); + if (json_object.contains("securityType"_L1) && json_object.contains("securityToken"_L1)) { + const QString security_type = json_object["securityType"_L1].toString(); + const QString security_token = json_object["securityToken"_L1].toString(); if (!security_type.isEmpty() && !security_token.isEmpty()) { - Error(tr("Received URL with encrypted stream from Tidal. Strawberry does not currently support encrypted streams.")); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, tr("Received URL with encrypted stream from Tidal. Strawberry does not currently support encrypted streams.")); return; } } if (urls.isEmpty()) { - Error(u"Missing stream urls."_s, json_obj); - Q_EMIT StreamURLFailure(id_, media_url_, errors_.constFirst()); + Q_EMIT StreamURLFailure(id_, media_url_, u"Missing stream urls."_s); return; } Q_EMIT StreamURLSuccess(id_, media_url_, urls.first(), filetype); } - -void TidalStreamURLRequest::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - - if (!error.isEmpty()) { - errors_ << error; - } - -} diff --git a/src/tidal/tidalstreamurlrequest.h b/src/tidal/tidalstreamurlrequest.h index 983449c0b..75fc39ee5 100644 --- a/src/tidal/tidalstreamurlrequest.h +++ b/src/tidal/tidalstreamurlrequest.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,21 +22,18 @@ #include "config.h" -#include -#include #include #include -#include #include +#include #include "includes/shared_ptr.h" #include "core/song.h" -#include "tidalservice.h" #include "tidalbaserequest.h" -#include "settings/tidalsettingspage.h" class QNetworkReply; class NetworkAccessManager; +class TidalService; class TidalStreamURLRequest : public TidalBaseRequest { Q_OBJECT @@ -49,10 +46,10 @@ class TidalStreamURLRequest : public TidalBaseRequest { void Process(); void Cancel(); - bool oauth() const { return service_->oauth(); } - TidalSettings::StreamUrlMethod stream_url_method() const { return service_->stream_url_method(); } - QUrl media_url() const { return media_url_; } - int song_id() const { return song_id_; } + bool oauth() const; + TidalSettings::StreamUrlMethod stream_url_method() const; + QUrl media_url() const; + int song_id() const; Q_SIGNALS: void StreamURLFailure(const uint id, const QUrl &media_url, const QString &error); @@ -62,15 +59,13 @@ class TidalStreamURLRequest : public TidalBaseRequest { void StreamURLReceived(); private: - void Error(const QString &error, const QVariant &debug = QVariant()) override; - TidalService *service_; QNetworkReply *reply_; QUrl media_url_; uint id_; int song_id_; - bool need_login_; - QStringList errors_; }; +using TidalStreamURLRequestPtr = QSharedPointer; + #endif // TIDALSTREAMURLREQUEST_H diff --git a/src/tidal/tidalurlhandler.cpp b/src/tidal/tidalurlhandler.cpp index 33796d2fa..3cca1a122 100644 --- a/src/tidal/tidalurlhandler.cpp +++ b/src/tidal/tidalurlhandler.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * 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,7 +19,6 @@ #include "config.h" -#include #include #include @@ -39,18 +38,24 @@ TidalUrlHandler::TidalUrlHandler(const SharedPtr task_manager, Tida } +QString TidalUrlHandler::scheme() const { + + return service_->url_scheme(); + +} + UrlHandler::LoadResult TidalUrlHandler::StartLoading(const QUrl &url) { - Request req; - req.task_id = task_manager_->StartTask(QStringLiteral("Loading %1 stream...").arg(url.scheme())); + Request request; + request.task_id = task_manager_->StartTask(QStringLiteral("Loading %1 stream...").arg(url.scheme())); QString error; - req.id = service_->GetStreamURL(url, error); - if (req.id == 0) { - CancelTask(req.task_id); + request.id = service_->GetStreamURL(url, error); + if (request.id == 0) { + CancelTask(request.task_id); return LoadResult(url, LoadResult::Type::Error, error); } - requests_.insert(req.id, req); + requests_.insert(request.id, request); LoadResult ret(url); ret.type_ = LoadResult::Type::WillLoadAsynchronously; diff --git a/src/tidal/tidalurlhandler.h b/src/tidal/tidalurlhandler.h index 5f08690e1..3826bcd03 100644 --- a/src/tidal/tidalurlhandler.h +++ b/src/tidal/tidalurlhandler.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2025, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,8 +22,6 @@ #include "config.h" -#include -#include #include #include #include @@ -31,9 +29,9 @@ #include "includes/shared_ptr.h" #include "core/urlhandler.h" #include "core/song.h" -#include "tidal/tidalservice.h" class TaskManager; +class TidalService; class TidalUrlHandler : public UrlHandler { Q_OBJECT @@ -41,7 +39,7 @@ class TidalUrlHandler : public UrlHandler { public: explicit TidalUrlHandler(const SharedPtr task_manager, TidalService *service); - QString scheme() const override { return service_->url_scheme(); } + QString scheme() const override; LoadResult StartLoading(const QUrl &url) override; private: diff --git a/src/providers/musixmatchprovider.cpp b/src/utilities/musixmatchprovider.cpp similarity index 86% rename from src/providers/musixmatchprovider.cpp rename to src/utilities/musixmatchprovider.cpp index 3c9b6212f..7356242b5 100644 --- a/src/providers/musixmatchprovider.cpp +++ b/src/utilities/musixmatchprovider.cpp @@ -24,10 +24,9 @@ using namespace Qt::Literals::StringLiterals; -const char *MusixmatchProvider::kApiUrl = "https://api.musixmatch.com/ws/1.1"; -const char *MusixmatchProvider::kApiKey = "Y2FhMDRlN2Y4OWE5OTIxYmZlOGMzOWQzOGI3ZGU4MjE="; +namespace MusixmatchProvider { -QString MusixmatchProvider::StringFixup(QString text) { +QString StringFixup(QString text) { static const QRegularExpression regex_illegal_characters(u"[^\\w0-9\\- ]"_s, QRegularExpression::UseUnicodePropertiesOption); static const QRegularExpression regex_duplicate_whitespaces(u" {2,}"_s); @@ -43,3 +42,5 @@ QString MusixmatchProvider::StringFixup(QString text) { .toLower(); } + +} // namespace diff --git a/src/providers/musixmatchprovider.h b/src/utilities/musixmatchprovider.h similarity index 84% rename from src/providers/musixmatchprovider.h rename to src/utilities/musixmatchprovider.h index 4412df511..e18235abc 100644 --- a/src/providers/musixmatchprovider.h +++ b/src/utilities/musixmatchprovider.h @@ -22,14 +22,10 @@ #include -class MusixmatchProvider { +namespace MusixmatchProvider { - protected: - static QString StringFixup(QString text); +QString StringFixup(QString text); - protected: - static const char *kApiUrl; - static const char *kApiKey; -}; +} // namespace #endif // MUSIXMATCHPROVIDER_H