diff --git a/src/lyrics/chartlyricsprovider.cpp b/src/lyrics/chartlyricsprovider.cpp index bb1a15a1f..fa73ebb3b 100644 --- a/src/lyrics/chartlyricsprovider.cpp +++ b/src/lyrics/chartlyricsprovider.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 @@ -46,17 +46,6 @@ constexpr char kUrlSearch[] = "http://api.chartlyrics.com/apiv1.asmx/SearchLyric ChartLyricsProvider::ChartLyricsProvider(const SharedPtr network, QObject *parent) : LyricsProvider(u"ChartLyrics"_s, false, false, network, parent) {} -ChartLyricsProvider::~ChartLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - void ChartLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { Q_ASSERT(QThread::currentThread() != qApp->thread()); @@ -65,12 +54,7 @@ void ChartLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &r url_query.addQueryItem(u"artist"_s, QString::fromUtf8(QUrl::toPercentEncoding(request.artist))); url_query.addQueryItem(u"song"_s, QString::fromUtf8(QUrl::toPercentEncoding(request.title))); - QUrl url(QString::fromUtf8(kUrlSearch)); - 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(kUrlSearch)), url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleSearchReply(reply, id, request); }); } @@ -82,25 +66,21 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - Q_EMIT SearchFinished(id); + const QScopeGuard search_finished = qScopeGuard([this, id]() { Q_EMIT SearchFinished(id); }); + + const ReplyDataResult reply_data_result = GetReplyData(reply); + if (!reply_data_result.success()) { + Error(reply_data_result.error_message); return; } - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - Q_EMIT SearchFinished(id); - return; - } - - QXmlStreamReader reader(reply); + QXmlStreamReader reader(reply_data_result.data); LyricsSearchResults results; LyricsSearchResult result; while (!reader.atEnd()) { - QXmlStreamReader::TokenType type = reader.readNext(); - QString name = reader.name().toString(); + const QXmlStreamReader::TokenType type = reader.readNext(); + const QString name = reader.name().toString(); if (type == QXmlStreamReader::StartElement) { if (name == "GetLyricResult"_L1) { result = LyricsSearchResult(); @@ -136,13 +116,4 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, qLog(Debug) << "ChartLyrics: Got lyrics for" << request.artist << request.title; } - Q_EMIT SearchFinished(id, results); - -} - -void ChartLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "ChartLyrics:" << error; - if (debug.isValid()) qLog(Debug) << debug; - } diff --git a/src/lyrics/chartlyricsprovider.h b/src/lyrics/chartlyricsprovider.h index 64b016cdc..4d15ba47c 100644 --- a/src/lyrics/chartlyricsprovider.h +++ b/src/lyrics/chartlyricsprovider.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,14 +22,11 @@ #include "config.h" -#include -#include #include #include #include "includes/shared_ptr.h" #include "lyricsprovider.h" -#include "lyricsfetcher.h" class QNetworkReply; class NetworkAccessManager; @@ -39,19 +36,12 @@ class ChartLyricsProvider : public LyricsProvider { public: explicit ChartLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~ChartLyricsProvider() override; - - private: - void Error(const QString &error, const QVariant &debug = QVariant()) override; protected Q_SLOTS: void StartSearch(const int id, const LyricsSearchRequest &request) override; private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); - - private: - QList replies_; }; #endif // CHARTLYRICSPROVIDER_H diff --git a/src/lyrics/geniuslyricsprovider.cpp b/src/lyrics/geniuslyricsprovider.cpp index 223321fda..8c25e8b4b 100644 --- a/src/lyrics/geniuslyricsprovider.cpp +++ b/src/lyrics/geniuslyricsprovider.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,12 +19,10 @@ #include "config.h" -#include #include #include #include -#include #include #include #include @@ -32,9 +30,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -48,9 +43,7 @@ #include "includes/shared_ptr.h" #include "core/logging.h" #include "core/networkaccessmanager.h" -#include "core/settings.h" -#include "core/localredirectserver.h" -#include "utilities/randutils.h" +#include "core/oauthenticator.h" #include "jsonlyricsprovider.h" #include "htmllyricsprovider.h" #include "geniuslyricsprovider.h" @@ -63,288 +56,162 @@ constexpr char kSettingsGroup[] = "GeniusLyrics"; constexpr char kOAuthAuthorizeUrl[] = "https://api.genius.com/oauth/authorize"; constexpr char kOAuthAccessTokenUrl[] = "https://api.genius.com/oauth/token"; constexpr char kOAuthRedirectUrl[] = "http://localhost:63111/"; // Genius does not accept a random port number. This port must match the URL of the ClientID. +constexpr char kOAuthScope[] = "me"; constexpr char kUrlSearch[] = "https://api.genius.com/search/"; constexpr char kClientIDB64[] = "RUNTNXU4U1VyMU1KUU5hdTZySEZteUxXY2hkanFiY3lfc2JjdXBpNG5WMU9SNUg4dTBZelEtZTZCdFg2dl91SQ=="; constexpr char kClientSecretB64[] = "VE9pMU9vUjNtTXZ3eFR3YVN0QVRyUjVoUlhVWDI1Ylp5X240eEt1M0ZkYlNwRG5JUnd0LXFFbHdGZkZkRWY2VzJ1S011UnQzM3c2Y3hqY0tVZ3NGN2c="; } // namespace -GeniusLyricsProvider::GeniusLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"Genius"_s, true, true, network, parent), server_(nullptr) { +GeniusLyricsProvider::GeniusLyricsProvider(const SharedPtr network, QObject *parent) + : JsonLyricsProvider(u"Genius"_s, true, true, network, parent), + oauth_(new OAuthenticator(network, this)) { - Settings s; - s.beginGroup(kSettingsGroup); - if (s.contains("access_token")) { - set_access_token(s.value("access_token").toString()); - } - s.endGroup(); + oauth_->set_settings_group(QLatin1String(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, &GeniusLyricsProvider::OAuthFinished); + + oauth_->LoadSession(); } -GeniusLyricsProvider::~GeniusLyricsProvider() { +bool GeniusLyricsProvider::authenticated() const { - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } + return oauth_->authenticated(); } -QString GeniusLyricsProvider::access_token() const { +bool GeniusLyricsProvider::use_authorization_header() const { - QMutexLocker l(&mutex_access_token_); - return access_token_; - -} - -void GeniusLyricsProvider::clear_access_token() { - - QMutexLocker l(&mutex_access_token_); - access_token_.clear(); - -} - -void GeniusLyricsProvider::set_access_token(const QString &access_token) { - - QMutexLocker l(&mutex_access_token_); - access_token_ = access_token; + return true; } void GeniusLyricsProvider::Authenticate() { - QUrl redirect_url(QString::fromLatin1(kOAuthRedirectUrl)); - - if (!server_) { - server_ = new LocalRedirectServer(this); - server_->set_port(redirect_url.port()); - if (!server_->Listen()) { - AuthError(server_->error()); - server_->deleteLater(); - server_ = nullptr; - return; - } - QObject::connect(server_, &LocalRedirectServer::Finished, this, &GeniusLyricsProvider::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); - } - - QUrlQuery url_query; - url_query.addQueryItem(u"client_id"_s, QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64))))); - url_query.addQueryItem(u"redirect_uri"_s, QString::fromLatin1(QUrl::toPercentEncoding(redirect_url.toString()))); - url_query.addQueryItem(u"scope"_s, u"me"_s); - url_query.addQueryItem(u"state"_s, QString::fromLatin1(QUrl::toPercentEncoding(code_challenge_))); - url_query.addQueryItem(u"response_type"_s, u"code"_s); - - QUrl url(QString::fromLatin1(kOAuthAuthorizeUrl)); - url.setQuery(url_query); - - const bool result = QDesktopServices::openUrl(url); - if (!result) { - QMessageBox messagebox(QMessageBox::Information, tr("Genius 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 GeniusLyricsProvider::RedirectArrived() { +void GeniusLyricsProvider::ClearSession() { - if (!server_) return; + oauth_->ClearSession(); - if (server_->error().isEmpty()) { - QUrl url = server_->request_url(); - if (url.isValid()) { - QUrlQuery url_query(url); - if (url_query.hasQueryItem(u"error"_s)) { - AuthError(QUrlQuery(url).queryItemValue(u"error"_s)); - } - else if (url_query.hasQueryItem(u"code"_s)) { - QUrl redirect_url(QString::fromLatin1(kOAuthRedirectUrl)); - redirect_url.setPort(server_->url().port()); - RequestAccessToken(url, redirect_url); - } - else { - AuthError(tr("Redirect missing token code!")); - } - } - else { - AuthError(tr("Received invalid reply from web browser.")); - } +} + +QByteArray GeniusLyricsProvider::authorization_header() const { + + return oauth_->authorization_header(); + +} + +void GeniusLyricsProvider::OAuthFinished(const bool success, const QString &error) { + + if (success) { + qLog(Debug) << "Genius: Authentication was successful."; + Q_EMIT AuthenticationComplete(true); + Q_EMIT AuthenticationSuccess(); } else { - AuthError(server_->error()); + qLog(Debug) << "Genius: Authentication failed."; + Q_EMIT AuthenticationFailure(error); + Q_EMIT AuthenticationComplete(false, error); } - server_->close(); - server_->deleteLater(); - server_ = nullptr; - -} - -void GeniusLyricsProvider::RequestAccessToken(const QUrl &url, const QUrl &redirect_url) { - - qLog(Debug) << "GeniusLyrics: Authorization URL Received" << url; - - QUrlQuery url_query(url); - - if (url.hasQuery() && url_query.hasQueryItem(u"code"_s) && url_query.hasQueryItem(u"state"_s)) { - - const QString code = url_query.queryItemValue(u"code"_s); - - QUrlQuery new_url_query; - new_url_query.addQueryItem(u"code"_s, QString::fromLatin1(QUrl::toPercentEncoding(code))); - new_url_query.addQueryItem(u"client_id"_s, QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64))))); - new_url_query.addQueryItem(u"client_secret"_s, QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64))))); - new_url_query.addQueryItem(u"redirect_uri"_s, QString::fromLatin1(QUrl::toPercentEncoding(redirect_url.toString()))); - new_url_query.addQueryItem(u"grant_type"_s, u"authorization_code"_s); - new_url_query.addQueryItem(u"response_type"_s, u"code"_s); - - QUrl new_url(QString::fromLatin1(kOAuthAccessTokenUrl)); - QNetworkRequest req(new_url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QByteArray query = new_url_query.toString(QUrl::FullyEncoded).toUtf8(); - - QNetworkReply *reply = network_->post(req, query); - replies_ << reply; - QObject::connect(reply, &QNetworkReply::sslErrors, this, &GeniusLyricsProvider::HandleLoginSSLErrors); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AccessTokenRequestFinished(reply); }); - - } - - else { - AuthError(tr("Redirect from Genius is missing query items code or state.")); - return; - } - -} - -void GeniusLyricsProvider::HandleLoginSSLErrors(const QList &ssl_errors) { - - for (const QSslError &ssl_error : ssl_errors) { - login_errors_ += ssl_error.errorString(); - } - -} - -void GeniusLyricsProvider::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. - AuthError(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. - const 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()); - } - } - AuthError(); - return; - } - } - - const QByteArray data = reply->readAll(); - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error(QStringLiteral("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString())); - return; - } - - if (json_doc.isEmpty()) { - AuthError(u"Authentication reply from server has empty Json document."_s); - return; - } - - if (!json_doc.isObject()) { - AuthError(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()) { - AuthError(u"Authentication reply from server has empty Json object."_s, json_doc); - return; - } - - if (!json_obj.contains("access_token"_L1)) { - AuthError(u"Authentication reply from server is missing access token."_s, json_obj); - return; - } - - const QString access_token = json_obj["access_token"_L1].toString(); - - set_access_token(access_token); - - Settings s; - s.beginGroup(kSettingsGroup); - s.setValue("access_token", access_token); - s.endGroup(); - - qLog(Debug) << "Genius: Authentication was successful."; - - Q_EMIT AuthenticationComplete(true); - Q_EMIT AuthenticationSuccess(); - } void GeniusLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { Q_ASSERT(QThread::currentThread() != qApp->thread()); + if (!authenticated()) { + EndSearch(id, request); + return; + } + GeniusLyricsSearchContextPtr search = make_shared(); search->id = id; search->request = request; requests_search_.insert(id, search); - if (access_token().isEmpty()) { - EndSearch(search); - return; - } - QUrlQuery url_query; url_query.addQueryItem(u"q"_s, QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral("%1 %2").arg(request.artist, request.title)))); - QUrl url(QString::fromLatin1(kUrlSearch)); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setRawHeader("Authorization", "Bearer " + access_token().toUtf8()); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kUrlSearch)), url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); }); } +GeniusLyricsProvider::JsonObjectResult GeniusLyricsProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return JsonObjectResult(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("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); + } + } + 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); + } + } + + if (reply->error() == QNetworkReply::AuthenticationRequiredError) { + oauth_->ClearSession(); + } + + return result; + +} + void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) { Q_ASSERT(QThread::currentThread() != qApp->thread()); @@ -357,59 +224,58 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) if (!requests_search_.contains(id)) return; GeniusLyricsSearchContextPtr search = requests_search_.value(id); - QJsonObject json_obj = ExtractJsonObj(reply); - if (json_obj.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; } - if (!json_obj.contains("meta"_L1)) { - Error(u"Json reply is missing meta object."_s, json_obj); - EndSearch(search); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj["meta"_L1].isObject()) { - Error(u"Json reply meta is not an object."_s, json_obj); - EndSearch(search); + + if (!json_object.contains("meta"_L1)) { + Error(u"Json reply is missing meta object."_s, json_object); return; } - QJsonObject obj_meta = json_obj["meta"_L1].toObject(); - if (!obj_meta.contains("status"_L1)) { - Error(u"Json reply meta object is missing status."_s, obj_meta); - EndSearch(search); + if (!json_object["meta"_L1].isObject()) { + Error(u"Json reply meta is not an object."_s, json_object); return; } - int status = obj_meta["status"_L1].toInt(); + const QJsonObject object_meta = json_object["meta"_L1].toObject(); + if (!object_meta.contains("status"_L1)) { + Error(u"Json reply meta object is missing status."_s, object_meta); + return; + } + const int status = object_meta["status"_L1].toInt(); if (status != 200) { - if (obj_meta.contains("message"_L1)) { - Error(QStringLiteral("Received error %1: %2.").arg(status).arg(obj_meta["message"_L1].toString())); + if (object_meta.contains("message"_L1)) { + Error(QStringLiteral("Received error %1: %2.").arg(status).arg(object_meta["message"_L1].toString())); } else { Error(QStringLiteral("Received error %1.").arg(status)); } - EndSearch(search); return; } - if (!json_obj.contains("response"_L1)) { - Error(u"Json reply is missing response."_s, json_obj); - EndSearch(search); + if (!json_object.contains("response"_L1)) { + Error(u"Json reply is missing response."_s, json_object); return; } - if (!json_obj["response"_L1].isObject()) { - Error(u"Json response is not an object."_s, json_obj); - EndSearch(search); + if (!json_object["response"_L1].isObject()) { + Error(u"Json response is not an object."_s, json_object); return; } - QJsonObject obj_response = json_obj["response"_L1].toObject(); + const QJsonObject obj_response = json_object["response"_L1].toObject(); if (!obj_response.contains("hits"_L1)) { Error(u"Json response is missing hits."_s, obj_response); - EndSearch(search); return; } if (!obj_response["hits"_L1].isArray()) { Error(u"Json hits is not an array."_s, obj_response); - EndSearch(search); return; } const QJsonArray array_hits = obj_response["hits"_L1].toArray(); @@ -418,23 +284,23 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) if (!value_hit.isObject()) { continue; } - QJsonObject obj_hit = value_hit.toObject(); - if (!obj_hit.contains("result"_L1)) { + const QJsonObject object_hit = value_hit.toObject(); + if (!object_hit.contains("result"_L1)) { continue; } - if (!obj_hit["result"_L1].isObject()) { + if (!object_hit["result"_L1].isObject()) { continue; } - QJsonObject obj_result = obj_hit["result"_L1].toObject(); - if (!obj_result.contains("title"_L1) || !obj_result.contains("primary_artist"_L1) || !obj_result.contains("url"_L1) || !obj_result["primary_artist"_L1].isObject()) { - Error(u"Missing one or more values in result object"_s, obj_result); + const QJsonObject object_result = object_hit["result"_L1].toObject(); + if (!object_result.contains("title"_L1) || !object_result.contains("primary_artist"_L1) || !object_result.contains("url"_L1) || !object_result["primary_artist"_L1].isObject()) { + Error(u"Missing one or more values in result object"_s, object_result); continue; } - QJsonObject primary_artist = obj_result["primary_artist"_L1].toObject(); + const QJsonObject primary_artist = object_result["primary_artist"_L1].toObject(); if (!primary_artist.contains("name"_L1)) continue; - QString artist = primary_artist["name"_L1].toString(); - QString title = obj_result["title"_L1].toString(); + const QString artist = primary_artist["name"_L1].toString(); + const QString title = object_result["title"_L1].toString(); // Ignore results where both the artist and title don't match. if (!artist.startsWith(search->request.albumartist, Qt::CaseInsensitive) && @@ -443,7 +309,7 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) continue; } - QUrl url(obj_result["url"_L1].toString()); + const QUrl url(object_result["url"_L1].toString()); if (!url.isValid()) continue; if (search->requests_lyric_.contains(url)) continue; @@ -454,16 +320,11 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id) search->requests_lyric_.insert(url, lyric); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *new_reply = network_->get(req); - replies_ << new_reply; + QNetworkReply *new_reply = CreateGetRequest(url); QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); }); } - EndSearch(search); - } void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url) { @@ -489,7 +350,7 @@ void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int sear EndSearch(search, lyric); return; } - else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); EndSearch(search, lyric); return; @@ -519,27 +380,6 @@ void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int sear } -void GeniusLyricsProvider::AuthError(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) login_errors_ << error; - - for (const QString &e : std::as_const(login_errors_)) Error(e); - if (debug.isValid()) qLog(Debug) << debug; - - Q_EMIT AuthenticationFailure(login_errors_); - Q_EMIT AuthenticationComplete(false, login_errors_); - - login_errors_.clear(); - -} - -void GeniusLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "GeniusLyrics:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} - void GeniusLyricsProvider::EndSearch(GeniusLyricsSearchContextPtr search, const GeniusLyricsLyricContext &lyric) { if (search->requests_lyric_.contains(lyric.url)) { @@ -547,13 +387,20 @@ void GeniusLyricsProvider::EndSearch(GeniusLyricsSearchContextPtr search, const } if (search->requests_lyric_.count() == 0) { requests_search_.remove(search->id); - if (search->results.isEmpty()) { - qLog(Debug) << "GeniusLyrics: No lyrics for" << search->request.artist << search->request.title; - } - else { - qLog(Debug) << "GeniusLyrics: Got lyrics for" << search->request.artist << search->request.title; - } - Q_EMIT SearchFinished(search->id, search->results); + EndSearch(search->id, search->request, search->results); } } + +void GeniusLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results) { + + if (results.isEmpty()) { + qLog(Debug) << "GeniusLyrics: No lyrics for" << request.artist << request.title; + } + else { + qLog(Debug) << "GeniusLyrics: Got lyrics for" << request.artist << request.title; + } + + Q_EMIT SearchFinished(id, results); + +} diff --git a/src/lyrics/geniuslyricsprovider.h b/src/lyrics/geniuslyricsprovider.h index 9b34dc374..949067f03 100644 --- a/src/lyrics/geniuslyricsprovider.h +++ b/src/lyrics/geniuslyricsprovider.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,16 +22,9 @@ #include "config.h" -#include -#include -#include #include -#include #include -#include #include -#include -#include #include #include "includes/shared_ptr.h" @@ -41,18 +34,20 @@ class QNetworkReply; class NetworkAccessManager; -class LocalRedirectServer; +class OAuthenticator; class GeniusLyricsProvider : public JsonLyricsProvider { Q_OBJECT public: explicit GeniusLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~GeniusLyricsProvider() override; - bool IsAuthenticated() const override { return !access_token().isEmpty(); } void Authenticate() override; - void Deauthenticate() override { clear_access_token(); } + void ClearSession() override; + + virtual bool authenticated() const override; + virtual bool use_authorization_header() const override; + virtual QByteArray authorization_header() const override; protected Q_SLOTS: void StartSearch(const int id, const LyricsSearchRequest &request) override; @@ -75,30 +70,19 @@ class GeniusLyricsProvider : public JsonLyricsProvider { using GeniusLyricsSearchContextPtr = SharedPtr; private: - QString access_token() const; - void clear_access_token(); - void set_access_token(const QString &access_token); - void RequestAccessToken(const QUrl &url, const QUrl &redirect_url); - void AuthError(const QString &error = QString(), const QVariant &debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; + JsonObjectResult ParseJsonObject(QNetworkReply *reply); void EndSearch(GeniusLyricsSearchContextPtr search, const GeniusLyricsLyricContext &lyric = GeniusLyricsLyricContext()); + void EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results = LyricsSearchResults()); private Q_SLOTS: - void HandleLoginSSLErrors(const QList &ssl_errors); - void RedirectArrived(); - void AccessTokenRequestFinished(QNetworkReply *reply); + void OAuthFinished(const bool success, const QString &error); void HandleSearchReply(QNetworkReply *reply, const int id); void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url); private: - LocalRedirectServer *server_; - QString code_verifier_; - QString code_challenge_; + OAuthenticator *oauth_; mutable QMutex mutex_access_token_; - QString access_token_; - QStringList login_errors_; QMap> requests_search_; - QList replies_; }; #endif // GENIUSLYRICSPROVIDER_H diff --git a/src/lyrics/htmllyricsprovider.cpp b/src/lyrics/htmllyricsprovider.cpp index db67ea088..4402415e2 100644 --- a/src/lyrics/htmllyricsprovider.cpp +++ b/src/lyrics/htmllyricsprovider.cpp @@ -41,17 +41,6 @@ using namespace Qt::Literals::StringLiterals; HtmlLyricsProvider::HtmlLyricsProvider(const QString &name, const bool enabled, const QString &start_tag, const QString &end_tag, const QString &lyrics_start, const bool multiple, const SharedPtr network, QObject *parent) : LyricsProvider(name, enabled, false, network, parent), start_tag_(start_tag), end_tag_(end_tag), lyrics_start_(lyrics_start), multiple_(multiple) {} -HtmlLyricsProvider::~HtmlLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - bool HtmlLyricsProvider::StartSearchAsync(const int id, const LyricsSearchRequest &request) { if (request.artist.isEmpty() || request.title.isEmpty()) return false; @@ -66,12 +55,8 @@ void HtmlLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &re Q_ASSERT(QThread::currentThread() != qApp->thread()); - QUrl url(Url(request)); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"_s); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + const QUrl url = Url(request); + QNetworkReply *reply = CreateGetRequest(url, true); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleLyricsReply(reply, id, request); }); qLog(Debug) << name_ << "Sending request for" << url; @@ -87,6 +72,10 @@ void HtmlLyricsProvider::HandleLyricsReply(QNetworkReply *reply, const int id, c QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); + LyricsSearchResults results; + + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); + if (reply->error() != QNetworkReply::NoError) { if (reply->error() == QNetworkReply::ContentNotFoundError) { qLog(Debug) << name_ << "No lyrics for" << request.artist << request.album << request.title; @@ -94,34 +83,29 @@ void HtmlLyricsProvider::HandleLyricsReply(QNetworkReply *reply, const int id, c else { qLog(Error) << name_ << reply->errorString() << reply->error(); } - Q_EMIT SearchFinished(id); return; } if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { qLog(Error) << name_ << "Received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - Q_EMIT SearchFinished(id); return; } - QByteArray data = reply->readAll(); + const QByteArray data = reply->readAll(); if (data.isEmpty()) { qLog(Error) << name_ << "Empty reply received from server."; - Q_EMIT SearchFinished(id); return; } const QString lyrics = ParseLyricsFromHTML(QString::fromUtf8(data), QRegularExpression(start_tag_), QRegularExpression(end_tag_), QRegularExpression(lyrics_start_), multiple_); if (lyrics.isEmpty() || lyrics.contains("we do not have the lyrics for"_L1, Qt::CaseInsensitive)) { qLog(Debug) << name_ << "No lyrics for" << request.artist << request.album << request.title; - Q_EMIT SearchFinished(id); return; } qLog(Debug) << name_ << "Got lyrics for" << request.artist << request.album << request.title; - LyricsSearchResult result(lyrics); - Q_EMIT SearchFinished(id, LyricsSearchResults() << result); + results << LyricsSearchResult(lyrics); } @@ -200,10 +184,3 @@ QString HtmlLyricsProvider::ParseLyricsFromHTML(const QString &content, const QR return Utilities::DecodeHtmlEntities(lyrics); } - -void HtmlLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << name_ << error; - if (debug.isValid()) qLog(Debug) << name_ << debug; - -} diff --git a/src/lyrics/htmllyricsprovider.h b/src/lyrics/htmllyricsprovider.h index 4595e820e..4022850dd 100644 --- a/src/lyrics/htmllyricsprovider.h +++ b/src/lyrics/htmllyricsprovider.h @@ -22,9 +22,6 @@ #include "config.h" -#include -#include -#include #include #include #include @@ -41,7 +38,6 @@ class HtmlLyricsProvider : public LyricsProvider { public: explicit HtmlLyricsProvider(const QString &name, const bool enabled, const QString &start_tag, const QString &end_tag, const QString &lyrics_start, const bool multiple, const SharedPtr network, QObject *parent); - ~HtmlLyricsProvider(); virtual bool StartSearchAsync(const int id, const LyricsSearchRequest &request) override; @@ -49,14 +45,12 @@ class HtmlLyricsProvider : public LyricsProvider { protected: virtual QUrl Url(const LyricsSearchRequest &request) = 0; - void Error(const QString &error, const QVariant &debug = QVariant()) override; protected Q_SLOTS: virtual void StartSearch(const int id, const LyricsSearchRequest &request) override; virtual void HandleLyricsReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); protected: - QList replies_; const QString start_tag_; const QString end_tag_; const QString lyrics_start_; diff --git a/src/lyrics/jsonlyricsprovider.cpp b/src/lyrics/jsonlyricsprovider.cpp index f770f907a..d50e590cf 100644 --- a/src/lyrics/jsonlyricsprovider.cpp +++ b/src/lyrics/jsonlyricsprovider.cpp @@ -30,60 +30,61 @@ #include "core/networkaccessmanager.h" #include "jsonlyricsprovider.h" -JsonLyricsProvider::JsonLyricsProvider(const QString &name, const bool enabled, const bool authentication_required, const SharedPtr network, QObject *parent) : LyricsProvider(name, enabled, authentication_required, network, parent) {} +using namespace Qt::Literals::StringLiterals; -QByteArray JsonLyricsProvider::ExtractData(QNetworkReply *reply) { +JsonLyricsProvider::JsonLyricsProvider(const QString &name, const bool enabled, const bool authentication_required, const SharedPtr network, QObject *parent) + : LyricsProvider(name, enabled, authentication_required, network, parent) {} - if (reply->error() != QNetworkReply::NoError) { - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - if (reply->error() < 200) { - return QByteArray(); - } - } - else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - } - - return reply->readAll(); - -} - -QJsonObject JsonLyricsProvider::ExtractJsonObj(const QByteArray &data) { - - if (data.isEmpty()) { - return QJsonObject(); - } +JsonLyricsProvider::JsonObjectResult JsonLyricsProvider::GetJsonObject(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())); - return QJsonObject(); + return JsonObjectResult(ErrorCode::ParseError, json_error.errorString()); } - if (json_doc.isEmpty()) { - Error(QStringLiteral("Received empty Json document."), data); - return QJsonObject(); + if (json_document.isEmpty()) { + return JsonObjectResult(ErrorCode::ParseError, "Received empty Json document."_L1); } - if (!json_doc.isObject()) { - Error(QStringLiteral("Json document is not an object."), json_doc); - return QJsonObject(); + if (!json_document.isObject()) { + return JsonObjectResult(ErrorCode::ParseError, "Json document is not an object."_L1); } - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error(QStringLiteral("Received empty Json object."), json_doc); - return QJsonObject(); + const QJsonObject json_object = json_document.object(); + if (json_object.isEmpty()) { + return JsonObjectResult(ErrorCode::ParseError, "Received empty Json object."_L1); } - return json_obj; + return json_object; } -QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply) { +JsonLyricsProvider::JsonObjectResult JsonLyricsProvider::GetJsonObject(QNetworkReply *reply) { - return ExtractJsonObj(ExtractData(reply)); + return GetJsonObject(reply->readAll()); + +} + +QJsonValue JsonLyricsProvider::GetJsonValue(const QJsonObject &json_object, const QString &name) { + + if (!json_object.contains(name)) { + Error(QStringLiteral("Json object is missing %1.").arg(name), json_object); + return QJsonArray(); + } + + return json_object[name]; + +} + +QJsonArray JsonLyricsProvider::GetJsonArray(const QJsonObject &json_object, const QString &name) { + + const QJsonValue json_value = GetJsonValue(json_object, name); + if (!json_value.isArray()) { + Error(QStringLiteral("%1 is not an array.").arg(name), json_object); + return QJsonArray(); + } + + return json_value.toArray(); } diff --git a/src/lyrics/jsonlyricsprovider.h b/src/lyrics/jsonlyricsprovider.h index 2624c6612..6f866e8c1 100644 --- a/src/lyrics/jsonlyricsprovider.h +++ b/src/lyrics/jsonlyricsprovider.h @@ -30,6 +30,7 @@ #include #include "includes/shared_ptr.h" +#include "core/jsonbaserequest.h" #include "lyricsprovider.h" class NetworkAccessManager; @@ -41,10 +42,19 @@ class JsonLyricsProvider : public LyricsProvider { public: explicit JsonLyricsProvider(const QString &name, const bool enabled, const bool authentication_required, const SharedPtr network, QObject *parent = nullptr); + class JsonObjectResult : public ReplyDataResult { + public: + JsonObjectResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {} + JsonObjectResult(const QJsonObject &_json_object) : ReplyDataResult(ErrorCode::Success), json_object(_json_object) {} + QJsonObject json_object; + }; + protected: - QByteArray ExtractData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(const QByteArray &data); - QJsonObject ExtractJsonObj(QNetworkReply *reply); + virtual JsonObjectResult GetJsonObject(const QByteArray &data); + virtual JsonObjectResult GetJsonObject(QNetworkReply *reply); + virtual QJsonValue GetJsonValue(const QJsonObject &json_object, const QString &name); + virtual QJsonArray GetJsonArray(const QJsonObject &json_object, const QString &name); + }; #endif // JSONLYRICSPROVIDER_H diff --git a/src/lyrics/letraslyricsprovider.h b/src/lyrics/letraslyricsprovider.h index 16b9186ea..88ff0115f 100644 --- a/src/lyrics/letraslyricsprovider.h +++ b/src/lyrics/letraslyricsprovider.h @@ -20,8 +20,6 @@ #ifndef LETRASLYRICSPROVIDER_H #define LETRASLYRICSPROVIDER_H -#include -#include #include #include diff --git a/src/lyrics/lololyricsprovider.cpp b/src/lyrics/lololyricsprovider.cpp index 8841a7fcb..ab31ca69c 100644 --- a/src/lyrics/lololyricsprovider.cpp +++ b/src/lyrics/lololyricsprovider.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 @@ -46,17 +46,6 @@ constexpr char kUrlSearch[] = "http://api.lololyrics.com/0.5/getLyric"; LoloLyricsProvider::LoloLyricsProvider(const SharedPtr network, QObject *parent) : LyricsProvider(u"LoloLyrics"_s, true, false, network, parent) {} -LoloLyricsProvider::~LoloLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - void LoloLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { Q_ASSERT(QThread::currentThread() != qApp->thread()); @@ -65,12 +54,7 @@ void LoloLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &re url_query.addQueryItem(u"artist"_s, QString::fromLatin1(QUrl::toPercentEncoding(request.artist))); url_query.addQueryItem(u"track"_s, QString::fromLatin1(QUrl::toPercentEncoding(request.title))); - QUrl url(QString::fromLatin1(kUrlSearch)); - 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(kUrlSearch)), url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleSearchReply(reply, id, request); }); } @@ -82,24 +66,19 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, c QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QString failure_reason; - if (reply->error() != QNetworkReply::NoError) { - failure_reason = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - if (reply->error() < 200) { - Error(failure_reason); - Q_EMIT SearchFinished(id); - return; - } - } - else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - failure_reason = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - - QByteArray data = reply->readAll(); LyricsSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); - if (!data.isEmpty()) { - QXmlStreamReader reader(data); + const ReplyDataResult reply_data_result = GetReplyData(reply); + if (!reply_data_result.success()) { + Error(reply_data_result.error_message); + return; + } + + QString error_message = reply_data_result.error_message; + + if (!reply_data_result.data.isEmpty()) { + QXmlStreamReader reader(reply_data_result.data); LyricsSearchResult result; QString status; while (!reader.atEnd()) { @@ -118,7 +97,7 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, c result.lyrics = reader.readElementText(); } else { - failure_reason = reader.readElementText(); + error_message = reader.readElementText(); result = LyricsSearchResult(); } } @@ -136,19 +115,10 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, c } if (results.isEmpty()) { - qLog(Debug) << "LoloLyrics: No lyrics for" << request.artist << request.title << failure_reason; + qLog(Debug) << "LoloLyrics: No lyrics for" << request.artist << request.title << error_message; } else { qLog(Debug) << "LoloLyrics: Got lyrics for" << request.artist << request.title; } - Q_EMIT SearchFinished(id, results); - -} - -void LoloLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "LoloLyrics:" << error; - if (debug.isValid()) qLog(Debug) << debug; - } diff --git a/src/lyrics/lololyricsprovider.h b/src/lyrics/lololyricsprovider.h index fe5a44e72..2efd4e100 100644 --- a/src/lyrics/lololyricsprovider.h +++ b/src/lyrics/lololyricsprovider.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 @@ -40,17 +37,10 @@ class LoloLyricsProvider : public LyricsProvider { public: explicit LoloLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~LoloLyricsProvider() override; - - private: - void Error(const QString &error, const QVariant &debug = QVariant()) override; private Q_SLOTS: void StartSearch(const int id, const LyricsSearchRequest &request) override; void HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); - - private: - QList replies_; }; #endif // LOLOLYRICSPROVIDER_H diff --git a/src/lyrics/lyricfindlyricsprovider.cpp b/src/lyrics/lyricfindlyricsprovider.cpp index f6985d1fa..cebf79b04 100644 --- a/src/lyrics/lyricfindlyricsprovider.cpp +++ b/src/lyrics/lyricfindlyricsprovider.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 @@ -47,17 +47,6 @@ constexpr char kLyricsEnd[] = ""; LyricFindLyricsProvider::LyricFindLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"lyricfind.com"_s, true, false, network, parent) {} -LyricFindLyricsProvider::~LyricFindLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - QUrl LyricFindLyricsProvider::Url(const LyricsSearchRequest &request) { return QUrl(QLatin1String(kUrl) + QLatin1Char('/') + StringFixup(request.artist) + QLatin1Char('-') + StringFixup(request.title)); @@ -85,11 +74,7 @@ void LyricFindLyricsProvider::StartSearch(const int id, const LyricsSearchReques Q_ASSERT(QThread::currentThread() != qApp->thread()); const QUrl url = Url(request); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - req.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"_s); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(url, true); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleSearchReply(reply, id, request); }); qLog(Debug) << "LyricFind: Sending request for" << url; @@ -108,20 +93,13 @@ void LyricFindLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int LyricsSearchResults results; const QScopeGuard end_search = qScopeGuard([this, id, request, &results]() { EndSearch(id, request, results); }); - if (reply->error() != QNetworkReply::NoError) { - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + const ReplyDataResult reply_data_result = GetReplyData(reply); + if (!reply_data_result.success()) { + Error(reply_data_result.error_message); return; } - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) { - const int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (http_code != 200 && http_code != 201 && http_code != 202) { - Error(QStringLiteral("Received HTTP code %1").arg(http_code)); - return; - } - } - - const QByteArray data = reply->readAll(); + const QByteArray &data = reply_data_result.data; if (data.isEmpty()) { Error(u"Empty reply received from server."_s); return; @@ -149,63 +127,63 @@ void LyricFindLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int return; } - QJsonObject obj = ExtractJsonObj(content_json.toUtf8()); - if (obj.isEmpty()) { + const JsonObjectResult json_object_result = GetJsonObject(content_json.toUtf8()); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - if (!obj.contains("props"_L1) || !obj["props"_L1].isObject()) { + + QJsonObject json_object = json_object_result.json_object; + if (json_object.isEmpty()) { + return; + } + + if (!json_object.contains("props"_L1) || !json_object["props"_L1].isObject()) { Error(u"Missing props."_s); return; } - obj = obj["props"_L1].toObject(); - if (!obj.contains("pageProps"_L1) || !obj["pageProps"_L1].isObject()) { + json_object = json_object["props"_L1].toObject(); + if (!json_object.contains("pageProps"_L1) || !json_object["pageProps"_L1].isObject()) { Error(u"Missing pageProps."_s); return; } - obj = obj["pageProps"_L1].toObject(); - if (!obj.contains("songData"_L1) || !obj["songData"_L1].isObject()) { + json_object = json_object["pageProps"_L1].toObject(); + if (!json_object.contains("songData"_L1) || !json_object["songData"_L1].isObject()) { Error(u"Missing songData."_s); return; } - obj = obj["songData"_L1].toObject(); + json_object = json_object["songData"_L1].toObject(); - if (!obj.contains("response"_L1) || !obj["response"_L1].isObject()) { + if (!json_object.contains("response"_L1) || !json_object["response"_L1].isObject()) { Error(u"Missing response."_s); return; } //const QJsonObject obj_response = obj[QLatin1String("response")].toObject(); - if (!obj.contains("track"_L1) || !obj["track"_L1].isObject()) { + if (!json_object.contains("track"_L1) || !json_object["track"_L1].isObject()) { Error(u"Missing track."_s); return; } - const QJsonObject obj_track = obj["track"_L1].toObject(); + const QJsonObject object_track = json_object["track"_L1].toObject(); - if (!obj_track.contains("title"_L1) || - !obj_track.contains("lyrics"_L1)) { + if (!object_track.contains("title"_L1) || + !object_track.contains("lyrics"_L1)) { Error(u"Missing title or lyrics."_s); return; } LyricsSearchResult result; - const QJsonObject obj_artist = obj["artist"_L1].toObject(); - if (obj_artist.contains("name"_L1)) { - result.artist = obj_artist["name"_L1].toString(); + const QJsonObject object_artist = json_object["artist"_L1].toObject(); + if (object_artist.contains("name"_L1)) { + result.artist = object_artist["name"_L1].toString(); } - result.title = obj_track["title"_L1].toString(); - result.lyrics = obj_track["lyrics"_L1].toString(); + result.title = object_track["title"_L1].toString(); + result.lyrics = object_track["lyrics"_L1].toString(); results << result; } -void LyricFindLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "LyricFind:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} - void LyricFindLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results) { if (results.isEmpty()) { diff --git a/src/lyrics/lyricfindlyricsprovider.h b/src/lyrics/lyricfindlyricsprovider.h index 3e67f5a1f..e292ba001 100644 --- a/src/lyrics/lyricfindlyricsprovider.h +++ b/src/lyrics/lyricfindlyricsprovider.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,8 +22,6 @@ #include "config.h" -#include -#include #include #include #include @@ -40,20 +38,15 @@ class LyricFindLyricsProvider : public JsonLyricsProvider { public: explicit LyricFindLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~LyricFindLyricsProvider() override; private: static QUrl Url(const LyricsSearchRequest &request); static QString StringFixup(const QString &text); void StartSearch(const int id, const LyricsSearchRequest &request) override; void EndSearch(const int id, const LyricsSearchRequest &request, const LyricsSearchResults &results = LyricsSearchResults()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); - - private: - QList replies_; }; #endif // LYRICFINDLYRICSPROVIDER_H diff --git a/src/lyrics/lyricsfetchersearch.cpp b/src/lyrics/lyricsfetchersearch.cpp index 7b3a168e0..f9ce9526e 100644 --- a/src/lyrics/lyricsfetchersearch.cpp +++ b/src/lyrics/lyricsfetchersearch.cpp @@ -73,7 +73,7 @@ void LyricsFetcherSearch::Start(SharedPtr lyrics_providers) { std::stable_sort(lyrics_providers_sorted.begin(), lyrics_providers_sorted.end(), ProviderCompareOrder); for (LyricsProvider *provider : std::as_const(lyrics_providers_sorted)) { - if (!provider->is_enabled() || !provider->IsAuthenticated()) continue; + if (!provider->is_enabled() || !provider->authenticated()) continue; QObject::connect(provider, &LyricsProvider::SearchFinished, this, &LyricsFetcherSearch::ProviderSearchFinished); const int id = lyrics_providers->NextId(); const bool success = provider->StartSearchAsync(id, request_); diff --git a/src/lyrics/lyricsprovider.cpp b/src/lyrics/lyricsprovider.cpp index 8a80b7ddd..c5859ecb0 100644 --- a/src/lyrics/lyricsprovider.cpp +++ b/src/lyrics/lyricsprovider.cpp @@ -26,7 +26,7 @@ #include "lyricsprovider.h" LyricsProvider::LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, const SharedPtr network, QObject *parent) - : QObject(parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required) {} + : HttpBaseRequest(network, parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required) {} bool LyricsProvider::StartSearchAsync(const int id, const LyricsSearchRequest &request) { diff --git a/src/lyrics/lyricsprovider.h b/src/lyrics/lyricsprovider.h index f4cc6dd8c..0d4671eb0 100644 --- a/src/lyrics/lyricsprovider.h +++ b/src/lyrics/lyricsprovider.h @@ -31,10 +31,11 @@ #include "includes/shared_ptr.h" #include "core/networkaccessmanager.h" +#include "core/httpbaserequest.h" #include "lyricssearchrequest.h" #include "lyricssearchresult.h" -class LyricsProvider : public QObject { +class LyricsProvider : public HttpBaseRequest { Q_OBJECT public: @@ -47,22 +48,24 @@ class LyricsProvider : public QObject { void set_enabled(const bool enabled) { enabled_ = enabled; } void set_order(const int order) { order_ = order; } + virtual QString service_name() const override { return name_; } + virtual bool authentication_required() const override { return authentication_required_; } + virtual bool authenticated() const override { return false; } + virtual bool use_authorization_header() const override { return authentication_required_; } + virtual QByteArray authorization_header() const override { return QByteArray(); } + virtual bool StartSearchAsync(const int id, const LyricsSearchRequest &request); virtual void CancelSearchAsync(const int id) { Q_UNUSED(id); } - virtual bool AuthenticationRequired() const { return authentication_required_; } virtual void Authenticate() {} - virtual bool IsAuthenticated() const { return !authentication_required_; } - virtual void Deauthenticate() {} - - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; + virtual void ClearSession() {} protected Q_SLOTS: virtual void StartSearch(const int id, const LyricsSearchRequest &request) = 0; Q_SIGNALS: - void AuthenticationComplete(const bool success, const QStringList &errors = QStringList()); + void AuthenticationComplete(const bool success, const QString &error = QString()); void AuthenticationSuccess(); - void AuthenticationFailure(const QStringList &errors); + void AuthenticationFailure(const QString &error); void SearchFinished(const int id, const LyricsSearchResults &results = LyricsSearchResults()); protected: diff --git a/src/lyrics/musixmatchlyricsprovider.cpp b/src/lyrics/musixmatchlyricsprovider.cpp index 514f068d6..3a3aa073a 100644 --- a/src/lyrics/musixmatchlyricsprovider.cpp +++ b/src/lyrics/musixmatchlyricsprovider.cpp @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2022, 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 @@ -56,17 +56,6 @@ constexpr char kApiKey[] = "Y2FhMDRlN2Y4OWE5OTIxYmZlOGMzOWQzOGI3ZGU4MjE="; MusixmatchLyricsProvider::MusixmatchLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"Musixmatch"_s, true, false, network, parent), use_api_(true) {} -MusixmatchLyricsProvider::~MusixmatchLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - void MusixmatchLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { Q_ASSERT(QThread::currentThread() != qApp->thread()); @@ -87,18 +76,13 @@ void MusixmatchLyricsProvider::StartSearch(const int id, const LyricsSearchReque bool MusixmatchLyricsProvider::SendSearchRequest(LyricsSearchContextPtr search) { + const QUrl url(QLatin1String(kApiUrl) + "/track.search"_L1); QUrlQuery url_query; url_query.addQueryItem(u"apikey"_s, QString::fromLatin1(QByteArray::fromBase64(kApiKey))); url_query.addQueryItem(u"q_artist"_s, QString::fromLatin1(QUrl::toPercentEncoding(search->request.artist))); url_query.addQueryItem(u"q_track"_s, QString::fromLatin1(QUrl::toPercentEncoding(search->request.title))); url_query.addQueryItem(u"f_has_lyrics"_s, u"1"_s); - - QUrl url(QString::fromLatin1(kApiUrl) + u"/track.search"_s); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + QNetworkReply *reply = CreateGetRequest(url, url_query); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search]() { HandleSearchReply(reply, search); }); qLog(Debug) << "MusixmatchLyrics: Sending request for" << url; @@ -107,71 +91,121 @@ bool MusixmatchLyricsProvider::SendSearchRequest(LyricsSearchContextPtr search) } +MusixmatchLyricsProvider::JsonObjectResult MusixmatchLyricsProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return JsonObjectResult(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("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); + } + } + 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 MusixmatchLyricsProvider::HandleSearchReply(QNetworkReply *reply, LyricsSearchContextPtr search) { Q_ASSERT(QThread::currentThread() != qApp->thread()); + const QScopeGuard end_search = qScopeGuard([this, search]() { EndSearch(search); }); + if (!replies_.contains(reply)) return; replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - if (reply->error() == 401 || reply->error() == 402) { - Error(QStringLiteral("Error %1 (%2) using API, switching to URL based lookup.").arg(reply->errorString()).arg(reply->error())); - use_api_ = false; - CreateLyricsRequest(search); - return; - } - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - EndSearch(search); + if (reply->error() == 401 || reply->error() == 402) { + Error(QStringLiteral("Error %1 (%2) using API, switching to URL based lookup.").arg(reply->errorString()).arg(reply->error())); + use_api_ = false; + CreateLyricsRequest(search); return; } - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401 || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 402) { - Error(QStringLiteral("Received HTTP code %1 using API, switching to URL based lookup.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - use_api_ = false; - CreateLyricsRequest(search); - return; - } - Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - EndSearch(search); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid() && (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 401 || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 402)) { + Error(QStringLiteral("Received HTTP code %1 using API, switching to URL based lookup.").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); + use_api_ = false; + CreateLyricsRequest(search); return; } - QByteArray data = reply->readAll(); - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - EndSearch(search); + const JsonObjectResult json_object_result = ParseJsonObject(reply); + if (!json_object_result.success()) { + Error(json_object_result.error_message); return; } - if (!json_obj.contains("message"_L1)) { - Error(u"Json reply is missing message object."_s, json_obj); - EndSearch(search); + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { return; } - if (!json_obj["message"_L1].isObject()) { - Error(u"Json reply message is not an object."_s, json_obj); - EndSearch(search); - return; - } - QJsonObject obj_message = json_obj["message"_L1].toObject(); - if (!obj_message.contains("header"_L1)) { - Error(u"Json reply message object is missing header."_s, obj_message); - EndSearch(search); + if (!json_object.contains("message"_L1)) { + Error(u"Json reply is missing message object."_s, json_object); return; } - if (!obj_message["header"_L1].isObject()) { - Error(u"Json reply message header is not an object."_s, obj_message); - EndSearch(search); + if (!json_object["message"_L1].isObject()) { + Error(u"Json reply message is not an object."_s, json_object); return; } - QJsonObject obj_header = obj_message["header"_L1].toObject(); + const QJsonObject object_message = json_object["message"_L1].toObject(); - int status_code = obj_header["status_code"_L1].toInt(); + if (!object_message.contains("header"_L1)) { + Error(u"Json reply message object is missing header."_s, object_message); + return; + } + if (!object_message["header"_L1].isObject()) { + Error(u"Json reply message header is not an object."_s, object_message); + return; + } + const QJsonObject object_header = object_message["header"_L1].toObject(); + + const int status_code = object_header["status_code"_L1].toInt(); if (status_code != 200) { Error(QStringLiteral("Received status code %1, switching to URL based lookup.").arg(status_code)); use_api_ = false; @@ -179,53 +213,49 @@ void MusixmatchLyricsProvider::HandleSearchReply(QNetworkReply *reply, LyricsSea return; } - if (!obj_message.contains("body"_L1)) { - Error(u"Json reply is missing body."_s, json_obj); - EndSearch(search); + if (!object_message.contains("body"_L1)) { + Error(u"Json reply is missing body."_s, json_object); return; } - if (!obj_message["body"_L1].isObject()) { - Error(u"Json body is not an object."_s, json_obj); - EndSearch(search); + if (!object_message["body"_L1].isObject()) { + Error(u"Json body is not an object."_s, json_object); return; } - QJsonObject obj_body = obj_message["body"_L1].toObject(); + const QJsonObject object_body = object_message["body"_L1].toObject(); - if (!obj_body.contains("track_list"_L1)) { - Error(u"Json response is missing body."_s, obj_body); - EndSearch(search); + if (!object_body.contains("track_list"_L1)) { + Error(u"Json response is missing body."_s, object_body); return; } - if (!obj_body["track_list"_L1].isArray()) { - Error(u"Json hits is not an array."_s, obj_body); - EndSearch(search); + if (!object_body["track_list"_L1].isArray()) { + Error(u"Json hits is not an array."_s, object_body); return; } - const QJsonArray array_tracklist = obj_body["track_list"_L1].toArray(); + const QJsonArray array_tracklist = object_body["track_list"_L1].toArray(); for (const QJsonValue &value_track : array_tracklist) { if (!value_track.isObject()) { continue; } - QJsonObject obj_track = value_track.toObject(); + QJsonObject object_track = value_track.toObject(); - if (!obj_track.contains("track"_L1) || !obj_track["track"_L1].isObject()) { + if (!object_track.contains("track"_L1) || !object_track["track"_L1].isObject()) { continue; } - obj_track = obj_track["track"_L1].toObject(); - if (!obj_track.contains("artist_name"_L1) || - !obj_track.contains("album_name"_L1) || - !obj_track.contains("track_name"_L1) || - !obj_track.contains("track_share_url"_L1)) { - Error(u"Missing one or more values in result object"_s, obj_track); + object_track = object_track["track"_L1].toObject(); + if (!object_track.contains("artist_name"_L1) || + !object_track.contains("album_name"_L1) || + !object_track.contains("track_name"_L1) || + !object_track.contains("track_share_url"_L1)) { + Error(u"Missing one or more values in result object"_s, object_track); continue; } - QString artist_name = obj_track["artist_name"_L1].toString(); - QString album_name = obj_track["album_name"_L1].toString(); - QString track_name = obj_track["track_name"_L1].toString(); - QUrl track_share_url(obj_track["track_share_url"_L1].toString()); + const QString artist_name = object_track["artist_name"_L1].toString(); + const QString album_name = object_track["album_name"_L1].toString(); + const QString track_name = object_track["track_name"_L1].toString(); + const QUrl track_share_url(object_track["track_share_url"_L1].toString()); // Ignore results where both the artist, album and title don't match. if (use_api_ && @@ -243,27 +273,22 @@ void MusixmatchLyricsProvider::HandleSearchReply(QNetworkReply *reply, LyricsSea } - if (search->requests_lyrics_.isEmpty()) { - EndSearch(search); - } - else { - for (const QUrl &url : std::as_const(search->requests_lyrics_)) { - SendLyricsRequest(search, url); - } + for (const QUrl &url : std::as_const(search->requests_lyrics_)) { + SendLyricsRequest(search, url); } } bool MusixmatchLyricsProvider::CreateLyricsRequest(LyricsSearchContextPtr search) { - QString artist_stripped = StringFixup(search->request.artist); - QString title_stripped = StringFixup(search->request.title); + const QString artist_stripped = StringFixup(search->request.artist); + const QString title_stripped = StringFixup(search->request.title); if (artist_stripped.isEmpty() || title_stripped.isEmpty()) { EndSearch(search); return false; } - QUrl url(QStringLiteral("https://www.musixmatch.com/lyrics/%1/%2").arg(artist_stripped, title_stripped)); + const QUrl url(QStringLiteral("https://www.musixmatch.com/lyrics/%1/%2").arg(artist_stripped, title_stripped)); search->requests_lyrics_.append(url); return SendLyricsRequest(search, url); @@ -271,14 +296,11 @@ bool MusixmatchLyricsProvider::CreateLyricsRequest(LyricsSearchContextPtr search bool MusixmatchLyricsProvider::SendLyricsRequest(LyricsSearchContextPtr search, const QUrl &url) { - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search, url]() { HandleLyricsReply(reply, search, url); }); - qLog(Debug) << "MusixmatchLyrics: Sending request for" << url; + QNetworkReply *reply = CreateGetRequest(url); + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search, url]() { HandleLyricsReply(reply, search, url); }); + return true; } @@ -287,30 +309,20 @@ void MusixmatchLyricsProvider::HandleLyricsReply(QNetworkReply *reply, LyricsSea Q_ASSERT(QThread::currentThread() != qApp->thread()); + const QScopeGuard end_search = qScopeGuard([this, search, url]() { EndSearch(search, url); }); + if (!replies_.contains(reply)) return; replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - EndSearch(search, url); - return; - } - if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); - EndSearch(search, url); + 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); - EndSearch(search, url); - return; - } - - const QString content = QString::fromUtf8(data); + const QString content = QString::fromUtf8(reply_data_result.data); const QString data_begin = ""_L1; qint64 begin_idx = content.indexOf(data_begin); @@ -324,75 +336,64 @@ void MusixmatchLyricsProvider::HandleLyricsReply(QNetworkReply *reply, LyricsSea } if (content_json.isEmpty()) { - EndSearch(search, url); return; } static const QRegularExpression regex_html_tag(u"<[^>]*>"_s); if (content_json.contains(regex_html_tag)) { // Make sure it's not HTML code. - EndSearch(search, url); return; } - QJsonObject obj_data = ExtractJsonObj(content_json.toUtf8()); - if (obj_data.isEmpty()) { - EndSearch(search, url); + QJsonObject object_data = GetJsonObject(content_json.toUtf8()).json_object; + if (object_data.isEmpty()) { return; } - if (!obj_data.contains("props"_L1) || !obj_data["props"_L1].isObject()) { - Error(u"Json reply is missing props."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("props"_L1) || !object_data["props"_L1].isObject()) { + Error(u"Json reply is missing props."_s, object_data); return; } - obj_data = obj_data["props"_L1].toObject(); + object_data = object_data["props"_L1].toObject(); - if (!obj_data.contains("pageProps"_L1) || !obj_data["pageProps"_L1].isObject()) { - Error(u"Json props is missing pageProps."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("pageProps"_L1) || !object_data["pageProps"_L1].isObject()) { + Error(u"Json props is missing pageProps."_s, object_data); return; } - obj_data = obj_data["pageProps"_L1].toObject(); + object_data = object_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); - EndSearch(search, url); + if (!object_data.contains("data"_L1) || !object_data["data"_L1].isObject()) { + Error(u"Json pageProps is missing data."_s, object_data); return; } - obj_data = obj_data["data"_L1].toObject(); + object_data = object_data["data"_L1].toObject(); - if (!obj_data.contains("trackInfo"_L1) || !obj_data["trackInfo"_L1].isObject()) { - Error(u"Json data is missing trackInfo."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("trackInfo"_L1) || !object_data["trackInfo"_L1].isObject()) { + Error(u"Json data is missing trackInfo."_s, object_data); return; } - obj_data = obj_data["trackInfo"_L1].toObject(); + object_data = object_data["trackInfo"_L1].toObject(); - if (!obj_data.contains("data"_L1) || !obj_data["data"_L1].isObject()) { - Error(u"Json trackInfo reply is missing data."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("data"_L1) || !object_data["data"_L1].isObject()) { + Error(u"Json trackInfo reply is missing data."_s, object_data); return; } - obj_data = obj_data["data"_L1].toObject(); + object_data = object_data["data"_L1].toObject(); - if (!obj_data.contains("track"_L1) || !obj_data["track"_L1].isObject()) { - Error(u"Json data is missing track."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("track"_L1) || !object_data["track"_L1].isObject()) { + Error(u"Json data is missing track."_s, object_data); return; } - const QJsonObject obj_track = obj_data["track"_L1].toObject(); + const QJsonObject obj_track = object_data["track"_L1].toObject(); if (!obj_track.contains("hasLyrics"_L1) || !obj_track["hasLyrics"_L1].isBool()) { Error(u"Json track is missing hasLyrics."_s, obj_track); - EndSearch(search, url); return; } const bool has_lyrics = obj_track["hasLyrics"_L1].toBool(); if (!has_lyrics) { - EndSearch(search, url); return; } @@ -407,32 +408,28 @@ void MusixmatchLyricsProvider::HandleLyricsReply(QNetworkReply *reply, LyricsSea result.title = obj_track["name"_L1].toString(); } - if (!obj_data.contains("lyrics"_L1) || !obj_data["lyrics"_L1].isObject()) { - Error(u"Json data is missing lyrics."_s, obj_data); - EndSearch(search, url); + if (!object_data.contains("lyrics"_L1) || !object_data["lyrics"_L1].isObject()) { + Error(u"Json data is missing lyrics."_s, object_data); return; } - QJsonObject obj_lyrics = obj_data["lyrics"_L1].toObject(); + const QJsonObject object_lyrics = object_data["lyrics"_L1].toObject(); - if (!obj_lyrics.contains("body"_L1) || !obj_lyrics["body"_L1].isString()) { - Error(u"Json lyrics reply is missing body."_s, obj_lyrics); - EndSearch(search, url); + if (!object_lyrics.contains("body"_L1) || !object_lyrics["body"_L1].isString()) { + Error(u"Json lyrics reply is missing body."_s, object_lyrics); return; } - result.lyrics = obj_lyrics["body"_L1].toString(); + result.lyrics = object_lyrics["body"_L1].toString(); if (!result.lyrics.isEmpty()) { result.lyrics = Utilities::DecodeHtmlEntities(result.lyrics); search->results.append(result); } - EndSearch(search, url); - } void MusixmatchLyricsProvider::EndSearch(LyricsSearchContextPtr search, const QUrl &url) { - if (search->requests_lyrics_.contains(url)) { + if (!url.isEmpty() && search->requests_lyrics_.contains(url)) { search->requests_lyrics_.removeAll(url); } @@ -448,10 +445,3 @@ void MusixmatchLyricsProvider::EndSearch(LyricsSearchContextPtr search, const QU } } - -void MusixmatchLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "MusixmatchLyrics:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/lyrics/musixmatchlyricsprovider.h b/src/lyrics/musixmatchlyricsprovider.h index 8d5828e2c..13f514ed3 100644 --- a/src/lyrics/musixmatchlyricsprovider.h +++ b/src/lyrics/musixmatchlyricsprovider.h @@ -1,6 +1,6 @@ /* * Strawberry Music Player - * Copyright 2020-2022, 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,10 +22,7 @@ #include "config.h" -#include -#include #include -#include #include #include @@ -42,7 +39,6 @@ class MusixmatchLyricsProvider : public JsonLyricsProvider { public: explicit MusixmatchLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~MusixmatchLyricsProvider() override; private: struct LyricsSearchContext { @@ -56,11 +52,11 @@ class MusixmatchLyricsProvider : public JsonLyricsProvider { using LyricsSearchContextPtr = SharedPtr; bool SendSearchRequest(LyricsSearchContextPtr search); + JsonObjectResult ParseJsonObject(QNetworkReply *reply); bool CreateLyricsRequest(LyricsSearchContextPtr search); void SendLyricsRequest(const LyricsSearchRequest &request, const QString &artist, const QString &title); bool SendLyricsRequest(LyricsSearchContextPtr search, const QUrl &url); void EndSearch(LyricsSearchContextPtr search, const QUrl &url = QUrl()); - void Error(const QString &error, const QVariant &debug = QVariant()) override; protected Q_SLOTS: void StartSearch(const int id, const LyricsSearchRequest &request) override; @@ -71,7 +67,6 @@ class MusixmatchLyricsProvider : public JsonLyricsProvider { private: QList requests_search_; - QList replies_; bool use_api_; }; diff --git a/src/lyrics/ovhlyricsprovider.cpp b/src/lyrics/ovhlyricsprovider.cpp index ae450633a..5535e7574 100644 --- a/src/lyrics/ovhlyricsprovider.cpp +++ b/src/lyrics/ovhlyricsprovider.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 @@ -41,77 +41,106 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr char kUrlSearch[] = "https://api.lyrics.ovh/v1/"; -} +} // namespace OVHLyricsProvider::OVHLyricsProvider(const SharedPtr network, QObject *parent) : JsonLyricsProvider(u"Lyrics.ovh"_s, true, false, network, parent) {} -OVHLyricsProvider::~OVHLyricsProvider() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - QObject::disconnect(reply, nullptr, this, nullptr); - reply->abort(); - reply->deleteLater(); - } - -} - void OVHLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &request) { Q_ASSERT(QThread::currentThread() != qApp->thread()); - QUrl url(QString::fromLatin1(kUrlSearch) + QString::fromLatin1(QUrl::toPercentEncoding(request.artist)) + QLatin1Char('/') + QString::fromLatin1(QUrl::toPercentEncoding(request.title))); - QNetworkRequest req(url); - req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = network_->get(req); - replies_ << reply; + const QUrl url(QLatin1String(kUrlSearch) + QString::fromLatin1(QUrl::toPercentEncoding(request.artist)) + '/'_L1 + QString::fromLatin1(QUrl::toPercentEncoding(request.title))); + QNetworkReply *reply = CreateGetRequest(url); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, request]() { HandleSearchReply(reply, id, request); }); } +OVHLyricsProvider::JsonObjectResult OVHLyricsProvider::ParseJsonObject(QNetworkReply *reply) { + + if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { + return JsonObjectResult(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)) { + result.error_code = ErrorCode::APIError; + result.error_message = json_object["error"_L1].toString(); + } + 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 OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request) { + LyricsSearchResults results; + const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); }); + if (!replies_.contains(reply)) return; replies_.removeAll(reply); QObject::disconnect(reply, nullptr, this, nullptr); reply->deleteLater(); - QJsonObject json_obj = ExtractJsonObj(reply); - if (json_obj.isEmpty()) { - Q_EMIT SearchFinished(id); + const JsonObjectResult json_object_reply = ParseJsonObject(reply); + if (!json_object_reply.success()) { + qLog(Debug) << "OVHLyrics" << json_object_reply.error_message; return; } - if (json_obj.contains("error"_L1)) { - Error(json_obj["error"_L1].toString()); + const QJsonObject &json_object = json_object_reply.json_object; + if (json_object.isEmpty()) { + return; + } + + if (json_object.contains("error"_L1)) { + Error(json_object["error"_L1].toString()); qLog(Debug) << "OVHLyrics: No lyrics for" << request.artist << request.title; - Q_EMIT SearchFinished(id); return; } - if (!json_obj.contains("lyrics"_L1)) { - Q_EMIT SearchFinished(id); + if (!json_object.contains("lyrics"_L1)) { return; } - LyricsSearchResult result; - result.lyrics = json_obj["lyrics"_L1].toString(); + const QString lyrics = json_object["lyrics"_L1].toString(); - if (result.lyrics.isEmpty()) { + if (lyrics.isEmpty()) { qLog(Debug) << "OVHLyrics: No lyrics for" << request.artist << request.title; - Q_EMIT SearchFinished(id); } else { - result.lyrics = Utilities::DecodeHtmlEntities(result.lyrics); qLog(Debug) << "OVHLyrics: Got lyrics for" << request.artist << request.title; - Q_EMIT SearchFinished(id, LyricsSearchResults() << result); + results << LyricsSearchResult(Utilities::DecodeHtmlEntities(lyrics)); } } - -void OVHLyricsProvider::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "OVHLyrics:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/lyrics/ovhlyricsprovider.h b/src/lyrics/ovhlyricsprovider.h index 75ff923ae..9b309999f 100644 --- a/src/lyrics/ovhlyricsprovider.h +++ b/src/lyrics/ovhlyricsprovider.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,6 @@ #include "config.h" -#include -#include -#include -#include -#include - #include "includes/shared_ptr.h" #include "jsonlyricsprovider.h" #include "lyricssearchrequest.h" @@ -40,19 +34,15 @@ class OVHLyricsProvider : public JsonLyricsProvider { public: explicit OVHLyricsProvider(const SharedPtr network, QObject *parent = nullptr); - ~OVHLyricsProvider() override; - - private: - void Error(const QString &error, const QVariant &debug = QVariant()) override; protected Q_SLOTS: void StartSearch(const int id, const LyricsSearchRequest &request) override; +private: + OVHLyricsProvider::JsonObjectResult ParseJsonObject(QNetworkReply *reply); + private Q_SLOTS: void HandleSearchReply(QNetworkReply *reply, const int id, const LyricsSearchRequest &request); - - private: - QList replies_; }; #endif // OVHLYRICSPROVIDER_H diff --git a/src/lyrics/songlyricscomlyricsprovider.h b/src/lyrics/songlyricscomlyricsprovider.h index dc7c48e6c..21331a9f8 100644 --- a/src/lyrics/songlyricscomlyricsprovider.h +++ b/src/lyrics/songlyricscomlyricsprovider.h @@ -20,8 +20,6 @@ #ifndef SONGLYRICSCOMLYRICSPROVIDER_H #define SONGLYRICSCOMLYRICSPROVIDER_H -#include -#include #include #include diff --git a/src/settings/lyricssettingspage.cpp b/src/settings/lyricssettingspage.cpp index ab03d8a64..6428974a2 100644 --- a/src/settings/lyricssettingspage.cpp +++ b/src/settings/lyricssettingspage.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 @@ -116,7 +116,7 @@ void LyricsSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QList if (item_previous) { LyricsProvider *provider = lyrics_providers_->ProviderByName(item_previous->text()); - if (provider && provider->AuthenticationRequired()) DisconnectAuthentication(provider); + if (provider && provider->authentication_required()) DisconnectAuthentication(provider); } if (item_current) { @@ -125,8 +125,8 @@ void LyricsSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QList ui_->providers_down->setEnabled(row != ui_->providers->count() - 1); LyricsProvider *provider = lyrics_providers_->ProviderByName(item_current->text()); if (provider) { - if (provider->AuthenticationRequired()) { - ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut); + if (provider->authentication_required()) { + 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(); @@ -227,7 +227,7 @@ void LyricsSettingsPage::LogoutClicked() { if (!ui_->providers->currentItem()) return; LyricsProvider *provider = lyrics_providers_->ProviderByName(ui_->providers->currentItem()->text()); if (!provider) return; - provider->Deauthenticate(); + provider->ClearSession(); ui_->button_authenticate->setEnabled(true); ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut); @@ -247,7 +247,7 @@ void LyricsSettingsPage::AuthenticationSuccess() { } -void LyricsSettingsPage::AuthenticationFailure(const QStringList &errors) { +void LyricsSettingsPage::AuthenticationFailure(const QString &error) { LyricsProvider *provider = qobject_cast(sender()); if (!provider) return; @@ -255,7 +255,7 @@ void LyricsSettingsPage::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/lyricssettingspage.h b/src/settings/lyricssettingspage.h index 9858368ef..96670c1be 100644 --- a/src/settings/lyricssettingspage.h +++ b/src/settings/lyricssettingspage.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 @@ -62,7 +62,7 @@ class LyricsSettingsPage : public SettingsPage { void AuthenticateClicked(); void LogoutClicked(); void AuthenticationSuccess(); - void AuthenticationFailure(const QStringList &errors); + void AuthenticationFailure(const QString &error); private: Ui_LyricsSettingsPage *ui_;