From ee7bb449a5ba921d61f4c980b5f9a15c9557e09c Mon Sep 17 00:00:00 2001 From: gitlost Date: Tue, 8 Jul 2025 22:03:49 +0100 Subject: [PATCH] Revert: Remove Genius lyrics [d9e38fb] --- CMakeLists.txt | 2 + README.md | 2 +- debian/control | 2 +- ...rawberrymusicplayer.strawberry.appdata.xml | 2 +- dist/unix/strawberry.1 | 2 +- dist/unix/strawberry.spec.in | 2 +- src/core/application.cpp | 2 + src/lyrics/geniuslyricsprovider.cpp | 406 ++++++++++++++++++ src/lyrics/geniuslyricsprovider.h | 88 ++++ 9 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 src/lyrics/geniuslyricsprovider.cpp create mode 100644 src/lyrics/geniuslyricsprovider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ca5395766..55fb5c03a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -682,6 +682,7 @@ set(SOURCES src/lyrics/htmllyricsprovider.cpp src/lyrics/ovhlyricsprovider.cpp src/lyrics/lololyricsprovider.cpp + src/lyrics/geniuslyricsprovider.cpp src/lyrics/musixmatchlyricsprovider.cpp src/lyrics/chartlyricsprovider.cpp src/lyrics/songlyricscomlyricsprovider.cpp @@ -979,6 +980,7 @@ set(HEADERS src/lyrics/htmllyricsprovider.h src/lyrics/ovhlyricsprovider.h src/lyrics/lololyricsprovider.h + src/lyrics/geniuslyricsprovider.h src/lyrics/musixmatchlyricsprovider.h src/lyrics/chartlyricsprovider.h src/lyrics/songlyricscomlyricsprovider.h diff --git a/README.md b/README.md index ceec3a8dd..73d02c22b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Funding developers is a way to contribute to open source projects you appreciate * Edit tags on audio files * Fetch tags from MusicBrainz * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/) - * Song lyrics from [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/) + * Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/) * Support for multiple backends * Audio analyzer * Audio equalizer diff --git a/debian/control b/debian/control index 5aacbfe86..c127b74f6 100644 --- a/debian/control +++ b/debian/control @@ -60,7 +60,7 @@ Description: music player and music collection organizer - Edit tags on audio files - Automatically retrieve tags from MusicBrainz - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - - Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net + - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net - Audio analyzer - Audio equalizer - Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml index 3826b9596..06d4ec5ad 100644 --- a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml +++ b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml @@ -31,7 +31,7 @@
  • Edit tags on audio files
  • Automatically retrieve tags from MusicBrainz
  • Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
  • -
  • Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
  • +
  • Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
  • Audio analyzer and equalizer
  • Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
  • Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
  • diff --git a/dist/unix/strawberry.1 b/dist/unix/strawberry.1 index 20696b666..c7cc71a50 100644 --- a/dist/unix/strawberry.1 +++ b/dist/unix/strawberry.1 @@ -29,7 +29,7 @@ Features: .br - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify .br -- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com +- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com .br - Support for multiple backends .br diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in index addd12538..2e66cc0a8 100644 --- a/dist/unix/strawberry.spec.in +++ b/dist/unix/strawberry.spec.in @@ -93,7 +93,7 @@ Features: - Edit tags on audio files - Automatically retrieve tags from MusicBrainz - Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify - - Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net + - Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net - Support for multiple backends - Audio analyzer - Audio equalizer diff --git a/src/core/application.cpp b/src/core/application.cpp index 3f1d5f170..a96f64330 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -64,6 +64,7 @@ #include "covermanager/opentidalcoverprovider.h" #include "lyrics/lyricsproviders.h" +#include "lyrics/geniuslyricsprovider.h" #include "lyrics/ovhlyricsprovider.h" #include "lyrics/lololyricsprovider.h" #include "lyrics/musixmatchlyricsprovider.h" @@ -172,6 +173,7 @@ class ApplicationImpl { lyrics_providers_([app]() { LyricsProviders *lyrics_providers = new LyricsProviders(app); // Initialize the repository of lyrics providers. + lyrics_providers->AddProvider(new GeniusLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new OVHLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network())); lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network())); diff --git a/src/lyrics/geniuslyricsprovider.cpp b/src/lyrics/geniuslyricsprovider.cpp new file mode 100644 index 000000000..8c25e8b4b --- /dev/null +++ b/src/lyrics/geniuslyricsprovider.cpp @@ -0,0 +1,406 @@ +/* + * Strawberry Music Player + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#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/oauthenticator.h" +#include "jsonlyricsprovider.h" +#include "htmllyricsprovider.h" +#include "geniuslyricsprovider.h" + +using namespace Qt::Literals::StringLiterals; +using std::make_shared; + +namespace { +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), + oauth_(new OAuthenticator(network, this)) { + + 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(); + +} + +bool GeniusLyricsProvider::authenticated() const { + + return oauth_->authenticated(); + +} + +bool GeniusLyricsProvider::use_authorization_header() const { + + return true; + +} + +void GeniusLyricsProvider::Authenticate() { + + oauth_->Authenticate(); + +} + +void GeniusLyricsProvider::ClearSession() { + + oauth_->ClearSession(); + +} + +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 { + qLog(Debug) << "Genius: Authentication failed."; + Q_EMIT AuthenticationFailure(error); + Q_EMIT AuthenticationComplete(false, error); + } + +} + +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); + + QUrlQuery url_query; + url_query.addQueryItem(u"q"_s, QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral("%1 %2").arg(request.artist, request.title)))); + + 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()); + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + QObject::disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + if (!requests_search_.contains(id)) return; + GeniusLyricsSearchContextPtr search = requests_search_.value(id); + + 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; + } + + const QJsonObject &json_object = json_object_result.json_object; + if (json_object.isEmpty()) { + return; + } + + if (!json_object.contains("meta"_L1)) { + Error(u"Json reply is missing meta object."_s, json_object); + return; + } + if (!json_object["meta"_L1].isObject()) { + Error(u"Json reply meta is not an object."_s, json_object); + return; + } + 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 (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)); + } + return; + } + + if (!json_object.contains("response"_L1)) { + Error(u"Json reply is missing response."_s, json_object); + return; + } + if (!json_object["response"_L1].isObject()) { + Error(u"Json response is not an object."_s, json_object); + return; + } + 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); + return; + } + if (!obj_response["hits"_L1].isArray()) { + Error(u"Json hits is not an array."_s, obj_response); + return; + } + const QJsonArray array_hits = obj_response["hits"_L1].toArray(); + + for (const QJsonValue &value_hit : array_hits) { + if (!value_hit.isObject()) { + continue; + } + const QJsonObject object_hit = value_hit.toObject(); + if (!object_hit.contains("result"_L1)) { + continue; + } + if (!object_hit["result"_L1].isObject()) { + continue; + } + 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; + } + const QJsonObject primary_artist = object_result["primary_artist"_L1].toObject(); + if (!primary_artist.contains("name"_L1)) continue; + + 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) && + !artist.startsWith(search->request.artist, Qt::CaseInsensitive) && + !title.startsWith(search->request.title, Qt::CaseInsensitive)) { + continue; + } + + const QUrl url(object_result["url"_L1].toString()); + if (!url.isValid()) continue; + if (search->requests_lyric_.contains(url)) continue; + + GeniusLyricsLyricContext lyric; + lyric.artist = artist; + lyric.title = title; + lyric.url = url; + + search->requests_lyric_.insert(url, lyric); + + QNetworkReply *new_reply = CreateGetRequest(url); + QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); }); + + } + +} + +void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url) { + + Q_ASSERT(QThread::currentThread() != qApp->thread()); + + if (!replies_.contains(reply)) return; + replies_.removeAll(reply); + QObject::disconnect(reply, nullptr, this, nullptr); + reply->deleteLater(); + + if (!requests_search_.contains(search_id)) return; + GeniusLyricsSearchContextPtr search = requests_search_.value(search_id); + + if (!search->requests_lyric_.contains(url)) { + EndSearch(search); + return; + } + const GeniusLyricsLyricContext lyric = search->requests_lyric_.value(url); + + if (reply->error() != QNetworkReply::NoError) { + Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + EndSearch(search, lyric); + return; + } + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); + EndSearch(search, lyric); + return; + } + + const QByteArray data = reply->readAll(); + if (data.isEmpty()) { + Error(u"Empty reply received from server."_s); + EndSearch(search, lyric); + return; + } + + const QString content = QString::fromUtf8(data); + QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"
    ]+>"_s), true); + if (lyrics.isEmpty()) { + lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"
    "_s), true); + } + + if (!lyrics.isEmpty()) { + LyricsSearchResult result(lyrics); + result.artist = lyric.artist; + result.title = lyric.title; + search->results.append(result); + } + + EndSearch(search, lyric); + +} + +void GeniusLyricsProvider::EndSearch(GeniusLyricsSearchContextPtr search, const GeniusLyricsLyricContext &lyric) { + + if (search->requests_lyric_.contains(lyric.url)) { + search->requests_lyric_.remove(lyric.url); + } + if (search->requests_lyric_.count() == 0) { + requests_search_.remove(search->id); + 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 new file mode 100644 index 000000000..949067f03 --- /dev/null +++ b/src/lyrics/geniuslyricsprovider.h @@ -0,0 +1,88 @@ +/* + * Strawberry Music Player + * 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef GENIUSLYRICSPROVIDER_H +#define GENIUSLYRICSPROVIDER_H + +#include "config.h" + +#include +#include +#include +#include + +#include "includes/shared_ptr.h" +#include "jsonlyricsprovider.h" +#include "lyricssearchrequest.h" +#include "lyricssearchresult.h" + +class QNetworkReply; +class NetworkAccessManager; +class OAuthenticator; + +class GeniusLyricsProvider : public JsonLyricsProvider { + Q_OBJECT + + public: + explicit GeniusLyricsProvider(const SharedPtr network, QObject *parent = nullptr); + + void Authenticate() override; + 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; + + private: + struct GeniusLyricsLyricContext { + explicit GeniusLyricsLyricContext() {} + QString artist; + QString title; + QUrl url; + }; + struct GeniusLyricsSearchContext { + explicit GeniusLyricsSearchContext() : id(-1) {} + int id; + LyricsSearchRequest request; + QMap requests_lyric_; + LyricsSearchResults results; + }; + + using GeniusLyricsSearchContextPtr = SharedPtr; + + private: + 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 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: + OAuthenticator *oauth_; + mutable QMutex mutex_access_token_; + QMap> requests_search_; +}; + +#endif // GENIUSLYRICSPROVIDER_H