diff --git a/README.md b/README.md index a6290dff3..33896688c 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay * Advanced audio output and device configuration for bit-perfect playback on Linux * Edit tags on music files * Fetch tags from MusicBrainz - * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/) and [Deezer](https://www.deezer.com/) - * Song lyrics from [AudD](https://audd.io/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/) + * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Deezer](https://www.deezer.com/) and [Tidal](https://www.tidal.com/) + * Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/) * Support for multiple backends * Audio analyzer * Audio equalizer * Transfer music to iPod, iPhone, MTP or mass-storage USB player * Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/) - * Subsonic streaming support + * Subsonic and Tidal streaming support It has so far been tested to work on Linux, OpenBSD and Windows. diff --git a/debian/control b/debian/control index 2e6acbaa3..64ae6c07f 100644 --- a/debian/control +++ b/debian/control @@ -56,7 +56,7 @@ Description: Audio player and music collection organizer - Edit tags on music files - Fetch tags from MusicBrainz - Album cover art from Lastfm, Musicbrainz, Discogs and Deezer - - Song lyrics from AudD, lyrics.ovh and lololyrics.com + - Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com - Support for multiple backends - Audio analyzer - Audio equalizer diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml index 41a1a6a0d..bf6269bbc 100644 --- a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml +++ b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml @@ -26,8 +26,8 @@
  • Advanced audio output and device configuration for bit-perfect playback on Linux
  • Edit tags on music files
  • Fetch tags from MusicBrainz
  • -
  • Album cover art from Last.fm, Musicbrainz and Discogs
  • -
  • Song lyrics from AudD, lyrics.ovh and lololyrics.com
  • +
  • Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal
  • +
  • Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
  • Support for multiple backends
  • Audio analyzer and equalizer
  • Transfer music to iPod, iPhone, MTP or mass-storage USB player
  • diff --git a/dist/unix/strawberry.1 b/dist/unix/strawberry.1 index 791f15efb..213cf3117 100644 --- a/dist/unix/strawberry.1 +++ b/dist/unix/strawberry.1 @@ -25,9 +25,9 @@ Features: .br - Fetch tags from MusicBrainz .br -- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer +- Album cover art from Lastfm, Musicbrainz, Discogs, Deezer and Tidal .br -- Song lyrics from AudD, lyrics.ovh and lololyrics.com +- Song lyrics from AudD, 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 129951ce4..4004edb79 100644 --- a/dist/unix/strawberry.spec.in +++ b/dist/unix/strawberry.spec.in @@ -104,8 +104,8 @@ Features: - Advanced audio output and device configuration for bit-perfect playback on Linux - Edit tags on music files - Fetch tags from MusicBrainz - - Album cover art from Last.fm, Musicbrainz, Discogs and Deezer - - Song lyrics from AudD, lyrics.ovh and lololyrics.com + - Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal + - Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com - Support for multiple backends - Audio analyzer - Audio equalizer diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1d42f0c9a..a91656b82 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -196,6 +196,9 @@ set(SOURCES lyrics/auddlyricsprovider.cpp lyrics/ovhlyricsprovider.cpp lyrics/lololyricsprovider.cpp + lyrics/geniuslyricsprovider.cpp + lyrics/musixmatchlyricsprovider.cpp + lyrics/chartlyricsprovider.cpp settings/settingsdialog.cpp settings/settingspage.cpp @@ -208,6 +211,7 @@ set(SOURCES settings/appearancesettingspage.cpp settings/notificationssettingspage.cpp settings/scrobblersettingspage.cpp + settings/lyricssettingspage.cpp dialogs/about.cpp dialogs/console.cpp @@ -384,6 +388,9 @@ set(HEADERS lyrics/auddlyricsprovider.h lyrics/ovhlyricsprovider.h lyrics/lololyricsprovider.h + lyrics/geniuslyricsprovider.h + lyrics/musixmatchlyricsprovider.h + lyrics/chartlyricsprovider.h settings/settingsdialog.h settings/settingspage.h @@ -396,6 +403,7 @@ set(HEADERS settings/appearancesettingspage.h settings/notificationssettingspage.h settings/scrobblersettingspage.h + settings/lyricssettingspage.h dialogs/about.h dialogs/errordialog.h @@ -492,6 +500,7 @@ set(UI settings/appearancesettingspage.ui settings/notificationssettingspage.ui settings/scrobblersettingspage.ui + settings/lyricssettingspage.ui equalizer/equalizer.ui equalizer/equalizerslider.ui diff --git a/src/core/application.cpp b/src/core/application.cpp index bc6655bec..1cae4f1c3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -60,8 +60,11 @@ #include "lyrics/lyricsproviders.h" #include "lyrics/auddlyricsprovider.h" +#include "lyrics/geniuslyricsprovider.h" #include "lyrics/ovhlyricsprovider.h" #include "lyrics/lololyricsprovider.h" +#include "lyrics/musixmatchlyricsprovider.h" +#include "lyrics/chartlyricsprovider.h" #include "scrobbler/audioscrobbler.h" @@ -132,8 +135,12 @@ class ApplicationImpl { lyrics_providers_([=]() { LyricsProviders *lyrics_providers = new LyricsProviders(app); lyrics_providers->AddProvider(new AuddLyricsProvider(app)); + lyrics_providers->AddProvider(new GeniusLyricsProvider(app)); lyrics_providers->AddProvider(new OVHLyricsProvider(app)); lyrics_providers->AddProvider(new LoloLyricsProvider(app)); + lyrics_providers->AddProvider(new MusixmatchLyricsProvider(app)); + lyrics_providers->AddProvider(new ChartLyricsProvider(app)); + lyrics_providers->ReloadSettings(); return lyrics_providers; }), internet_services_([=]() { diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index e1eab4628..6512c1a33 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -134,6 +134,7 @@ #include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverloaderresult.h" #include "covermanager/currentalbumcoverloader.h" +#include "lyrics/lyricsproviders.h" #ifndef Q_OS_WIN # include "device/devicemanager.h" # include "device/devicestatefiltermodel.h" @@ -950,6 +951,7 @@ void MainWindow::ReloadAllSettings() { album_cover_choice_controller_->ReloadSettings(); if (cover_manager_.get()) cover_manager_->ReloadSettings(); context_view_->ReloadSettings(); + app_->lyrics_providers()->ReloadSettings(); #ifdef HAVE_SUBSONIC subsonic_view_->ReloadSettings(); #endif diff --git a/src/internet/localredirectserver.cpp b/src/internet/localredirectserver.cpp index eb1247e41..c5ef60397 100644 --- a/src/internet/localredirectserver.cpp +++ b/src/internet/localredirectserver.cpp @@ -49,9 +49,10 @@ #include #include -LocalRedirectServer::LocalRedirectServer(const bool https, QObject *parent) +LocalRedirectServer::LocalRedirectServer(QObject *parent) : QTcpServer(parent), - https_(https), + https_(false), + port_(0), socket_(nullptr) {} @@ -232,7 +233,7 @@ bool LocalRedirectServer::Listen() { if (https_) { if (!GenerateCertificate()) return false; } - if (!listen(QHostAddress::LocalHost)) { + if (!listen(QHostAddress::LocalHost, port_)) { error_ = errorString(); return false; } diff --git a/src/internet/localredirectserver.h b/src/internet/localredirectserver.h index 4200fcffe..23533056f 100644 --- a/src/internet/localredirectserver.h +++ b/src/internet/localredirectserver.h @@ -38,9 +38,11 @@ class LocalRedirectServer : public QTcpServer { Q_OBJECT public: - explicit LocalRedirectServer(const bool https, QObject* parent = nullptr); + explicit LocalRedirectServer(QObject *parent = nullptr); ~LocalRedirectServer(); + void set_https(const bool https) { https_ = https; } + void set_port(const int port) { port_ = port; } bool Listen(); const QUrl &url() const { return url_; } const QUrl &request_url() const { return request_url_; } @@ -65,6 +67,7 @@ class LocalRedirectServer : public QTcpServer { private: bool https_; + int port_; QUrl url_; QUrl request_url_; QSslCertificate ssl_certificate_; diff --git a/src/lyrics/auddlyricsprovider.cpp b/src/lyrics/auddlyricsprovider.cpp index ba5eddacd..97687dd40 100644 --- a/src/lyrics/auddlyricsprovider.cpp +++ b/src/lyrics/auddlyricsprovider.cpp @@ -46,7 +46,7 @@ const char *AuddLyricsProvider::kUrlSearch = "https://api.audd.io/findLyrics/"; const char *AuddLyricsProvider::kAPITokenB64 = "ZjA0NjQ4YjgyNDM3ZTc1MjY3YjJlZDI5ZDBlMzQxZjk="; const int AuddLyricsProvider::kMaxLength = 6000; -AuddLyricsProvider::AuddLyricsProvider(QObject *parent) : JsonLyricsProvider("AudD", parent), network_(new NetworkAccessManager(this)) {} +AuddLyricsProvider::AuddLyricsProvider(QObject *parent) : JsonLyricsProvider("AudD", true, false, parent), network_(new NetworkAccessManager(this)) {} bool AuddLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) { @@ -79,8 +79,9 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i reply->deleteLater(); - QJsonArray json_result = ExtractResult(reply, id, artist, title); + QJsonArray json_result = ExtractResult(reply, artist, title); if (json_result.isEmpty()) { + emit SearchFinished(id, LyricsSearchResults()); return; } @@ -110,10 +111,7 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i result.lyrics = json_obj["lyrics"].toString(); if (result.lyrics.length() > kMaxLength) continue; if (result.lyrics == "error") continue; - result.score = 0.0; - if (result.artist.toLower() == artist.toLower()) result.score += 1.0; - if (result.title.toLower() == title.toLower()) result.score += 1.0; - if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength) result.score += 1.0; + //qLog(Debug) << "AudDLyrics:" << result.artist << result.title << result.lyrics.length(); results << result; @@ -126,40 +124,40 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i } -QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) { +QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const QString &artist, const QString &title) { - QJsonObject json_obj = ExtractJsonObj(reply, id); + QJsonObject json_obj = ExtractJsonObj(reply); if (json_obj.isEmpty()) return QJsonArray(); if (!json_obj.contains("status")) { - Error(id, "Json reply is missing status.", json_obj); + Error("Json reply is missing status.", json_obj); return QJsonArray(); } if (json_obj["status"].toString() == "error") { if (!json_obj.contains("error")) { - Error(id, "Json reply is missing error status.", json_obj); + Error("Json reply is missing error status.", json_obj); return QJsonArray(); } QJsonObject json_error = json_obj["error"].toObject(); if (!json_error.contains("error_code") || !json_error.contains("error_message")) { - Error(id, "Json reply is missing error code or message.", json_error); + Error("Json reply is missing error code or message.", json_error); return QJsonArray(); } QString error_code(json_error["error_code"].toString()); QString error_message(json_error["error_message"].toString()); - Error(id, error_message); + Error(error_message); return QJsonArray(); } if (!json_obj.contains("result")) { - Error(id, "Json reply is missing result.", json_obj); + Error("Json reply is missing result.", json_obj); return QJsonArray(); } QJsonArray json_result = json_obj["result"].toArray(); if (json_result.isEmpty()) { - Error(id, QString("No lyrics for %1 %2").arg(artist).arg(title)); + Error(QString("No lyrics for %1 %2").arg(artist).arg(title)); return QJsonArray(); } @@ -167,8 +165,9 @@ QJsonArray AuddLyricsProvider::ExtractResult(QNetworkReply *reply, const quint64 } -void AuddLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) { +void AuddLyricsProvider::Error(const QString &error, const QVariant &debug) { + qLog(Error) << "AudDLyrics:" << error; if (debug.isValid()) qLog(Debug) << debug; - emit SearchFinished(id, LyricsSearchResults()); + } diff --git a/src/lyrics/auddlyricsprovider.h b/src/lyrics/auddlyricsprovider.h index a9cea39f9..39c484718 100644 --- a/src/lyrics/auddlyricsprovider.h +++ b/src/lyrics/auddlyricsprovider.h @@ -51,9 +51,9 @@ class AuddLyricsProvider : public JsonLyricsProvider { static const char *kAPITokenB64; static const int kMaxLength; QNetworkAccessManager *network_; - void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); - QJsonArray ExtractResult(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title); + QJsonArray ExtractResult(QNetworkReply *reply, const QString &artist, const QString &title); }; diff --git a/src/lyrics/chartlyricsprovider.cpp b/src/lyrics/chartlyricsprovider.cpp new file mode 100644 index 000000000..d5101614d --- /dev/null +++ b/src/lyrics/chartlyricsprovider.cpp @@ -0,0 +1,129 @@ +/* + * Strawberry Music Player + * Copyright 2018, 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 "core/closure.h" +#include "core/logging.h" +#include "core/network.h" +#include "core/utilities.h" +#include "lyricsprovider.h" +#include "lyricsfetcher.h" +#include "chartlyricsprovider.h" + +const char *ChartLyricsProvider::kUrlSearch = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect"; + +ChartLyricsProvider::ChartLyricsProvider(QObject *parent) : LyricsProvider("ChartLyrics", false, false, parent), network_(new NetworkAccessManager(this)) {} + +bool ChartLyricsProvider::StartSearch(const QString &artist, const QString&, const QString &title, const quint64 id) { + + const ParamList params = ParamList() << Param("artist", artist) + << Param("song", title); + + QUrlQuery url_query; + for (const Param ¶m : params) { + url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl url(kUrlSearch); + url.setQuery(url_query); + QNetworkReply *reply = network_->get(QNetworkRequest(url)); + NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, const quint64, const QString&, const QString&)), reply, id, artist, title); + + //qLog(Debug) << "ChartLyrics: Sending request for" << url; + + return true; + +} + +void ChartLyricsProvider::CancelSearch(const quint64) {} + +void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title) { + + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + emit SearchFinished(id, LyricsSearchResults()); + return; + } + + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); + emit SearchFinished(id, LyricsSearchResults()); + return; + } + + QXmlStreamReader reader(reply); + LyricsSearchResults results; + LyricsSearchResult result; + + while (!reader.atEnd()) { + QXmlStreamReader::TokenType type = reader.readNext(); + QStringRef name = reader.name(); + if (type == QXmlStreamReader::StartElement) { + if (name == "GetLyricResult") { + result = LyricsSearchResult(); + } + if (name == "LyricArtist") { + result.artist = reader.readElementText(); + } + else if (name == "LyricSong") { + result.title = reader.readElementText(); + } + else if (name == "Lyric") { + result.lyrics = reader.readElementText(); + } + } + else if (type == QXmlStreamReader::EndElement) { + if (name == "GetLyricResult") { + if (!result.artist.isEmpty() && !result.title.isEmpty() && !result.lyrics.isEmpty() && (result.artist.toLower() == artist.toLower() || result.title.toLower() == title.toLower())) { + results << result; + } + result = LyricsSearchResult(); + } + } + } + + if (results.isEmpty()) qLog(Debug) << "ChartLyrics: No lyrics for" << artist << title; + else qLog(Debug) << "ChartLyrics: Got lyrics for" << artist << title; + + emit SearchFinished(id, results); + +} + +void ChartLyricsProvider::Error(const QString &error, QVariant debug) { + + qLog(Error) << "ChartLyrics:" << error; + if (debug.isValid()) qLog(Debug) << debug; + +} diff --git a/src/lyrics/chartlyricsprovider.h b/src/lyrics/chartlyricsprovider.h new file mode 100644 index 000000000..cfa4e3033 --- /dev/null +++ b/src/lyrics/chartlyricsprovider.h @@ -0,0 +1,56 @@ +/* + * Strawberry Music Player + * Copyright 2018, 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 CHARTLYRICSPROVIDER_H +#define CHARTLYRICSPROVIDER_H + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "lyricsprovider.h" +#include "lyricsfetcher.h" + +class ChartLyricsProvider : public LyricsProvider { + Q_OBJECT + + public: + explicit ChartLyricsProvider(QObject *parent = nullptr); + + bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id); + void CancelSearch(quint64 id); + + private: + void Error(const QString &error, QVariant debug = QVariant()); + + private slots: + void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &title); + + private: + static const char *kUrlSearch; + + QNetworkAccessManager *network_; + +}; + +#endif // CHARTLYRICSPROVIDER_H diff --git a/src/lyrics/geniuslyricsprovider.cpp b/src/lyrics/geniuslyricsprovider.cpp new file mode 100644 index 000000000..94c42cb0d --- /dev/null +++ b/src/lyrics/geniuslyricsprovider.cpp @@ -0,0 +1,536 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/utilities.h" +#include "core/network.h" +#include "internet/localredirectserver.h" +#include "jsonlyricsprovider.h" +#include "lyricsfetcher.h" +#include "lyricsprovider.h" +#include "geniuslyricsprovider.h" + +const char *GeniusLyricsProvider::kSettingsGroup = "GeniusLyrics"; +const char *GeniusLyricsProvider::kOAuthAuthorizeUrl = "https://api.genius.com/oauth/authorize"; +const char *GeniusLyricsProvider::kOAuthAccessTokenUrl = "https://api.genius.com/oauth/token"; +const char *GeniusLyricsProvider::kOAuthRedirectUrl = "http://localhost:63111/"; // Genius does not accept a random port number. This port must match the the URL of the ClientID. +const char *GeniusLyricsProvider::kUrlSearch = "https://api.genius.com/search/"; +const char *GeniusLyricsProvider::kClientIDB64 = "RUNTNXU4U1VyMU1KUU5hdTZySEZteUxXY2hkanFiY3lfc2JjdXBpNG5WMU9SNUg4dTBZelEtZTZCdFg2dl91SQ=="; +const char *GeniusLyricsProvider::kClientSecretB64 = "VE9pMU9vUjNtTXZ3eFR3YVN0QVRyUjVoUlhVWDI1Ylp5X240eEt1M0ZkYlNwRG5JUnd0LXFFbHdGZkZkRWY2VzJ1S011UnQzM3c2Y3hqY0tVZ3NGN2c="; + +GeniusLyricsProvider::GeniusLyricsProvider(QObject *parent) : JsonLyricsProvider("Genius", true, true, parent), network_(new NetworkAccessManager(this)), server_(nullptr) { + + QSettings s; + s.beginGroup(kSettingsGroup); + if (s.contains("access_token")) { + access_token_ = s.value("access_token").toString(); + } + s.endGroup(); + +} + +void GeniusLyricsProvider::Authenticate() { + + QUrl redirect_url(kOAuthRedirectUrl); + + if (!server_) { + server_ = new LocalRedirectServer(this); + server_->set_https(false); + server_->set_port(redirect_url.port()); + if (!server_->Listen()) { + AuthError(server_->error()); + server_->deleteLater(); + server_ = nullptr; + return; + } + connect(server_, SIGNAL(Finished()), this, SLOT(RedirectArrived())); + } + + code_verifier_ = Utilities::CryptographicRandomString(44); + code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding)); + if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) { + code_challenge_.chop(1); + } + + const ParamList params = ParamList() << Param("client_id", QByteArray::fromBase64(kClientIDB64)) + << Param("redirect_uri", redirect_url.toString()) + << Param("scope", "me") + << Param("state", code_challenge_) + << Param("response_type", "code"); + + QUrlQuery url_query; + for (const Param ¶m : params) { + url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl url(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") + QString(":
    %1").arg(url.toString()), QMessageBox::Ok); + messagebox.setTextFormat(Qt::RichText); + messagebox.exec(); + } + +} + +void GeniusLyricsProvider::RedirectArrived() { + + if (!server_) return; + + if (server_->error().isEmpty()) { + QUrl url = server_->request_url(); + if (url.isValid()) { + QUrlQuery url_query(url); + if (url_query.hasQueryItem("error")) { + AuthError(QUrlQuery(url).queryItemValue("error")); + } + else if (url_query.hasQueryItem("code")) { + QUrl redirect_url(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.")); + } + } + else { + AuthError(server_->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("code") && url_query.hasQueryItem("state")) { + + QString code = url_query.queryItemValue("code"); + QString state = url_query.queryItemValue("state"); + + const ParamList params = ParamList() << Param("code", code) + << Param("client_id", QByteArray::fromBase64(kClientIDB64)) + << Param("client_secret", QByteArray::fromBase64(kClientSecretB64)) + << Param("redirect_uri", redirect_url.toString()) + << Param("grant_type", "authorization_code") + << Param("response_type", "code"); + + QUrlQuery new_url_query; + for (const Param ¶m : params) { + new_url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl new_url(kOAuthAccessTokenUrl); + QNetworkRequest req = QNetworkRequest(new_url); + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QByteArray query = new_url_query.toString(QUrl::FullyEncoded).toUtf8(); + + QNetworkReply *reply = network_->post(req, query); + connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); + connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); }); + + } + + else { + AuthError(tr("Redirect from Genius is missing query items code or state.")); + return; + } + +} + +void GeniusLyricsProvider::HandleLoginSSLErrors(QList ssl_errors) { + + for (const QSslError &ssl_error : ssl_errors) { + login_errors_ += ssl_error.errorString(); + } + +} + +void GeniusLyricsProvider::AccessTokenRequestFinished(QNetworkReply *reply) { + + 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(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + return; + } + else { + // See if there is Json data containing "status" and "userMessage" then use that instead. + QByteArray data(reply->readAll()); + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (!json_obj.isEmpty() && json_obj.contains("error") && json_obj.contains("error_description")) { + QString error = json_obj["error"].toString(); + QString error_description = json_obj["error_description"].toString(); + login_errors_ << QString("Authentication failure: %1 (%2)").arg(error).arg(error_description); + } + } + if (login_errors_.isEmpty()) { + if (reply->error() != QNetworkReply::NoError) { + login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + } + else { + login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + } + } + AuthError(); + return; + } + } + + QByteArray data(reply->readAll()); + + QJsonParseError json_error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); + + if (json_error.error != QJsonParseError::NoError) { + AuthError("Authentication reply from server missing Json data."); + return; + } + + if (json_doc.isEmpty()) { + AuthError("Authentication reply from server has empty Json document."); + return; + } + + if (!json_doc.isObject()) { + AuthError("Authentication reply from server has Json document that is not an object.", json_doc); + return; + } + + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + AuthError("Authentication reply from server has empty Json object.", json_doc); + return; + } + + if (!json_obj.contains("access_token")) { + AuthError("Authentication reply from server is missing access token.", json_obj); + return; + } + + access_token_ = json_obj["access_token"].toString(); + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("access_token", access_token_); + s.endGroup(); + + qLog(Debug) << "Genius: Authentication was successful, got access token" << access_token_; + + emit AuthenticationComplete(true); + emit AuthenticationSuccess(); + +} + +bool GeniusLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) { + + Q_UNUSED(album); + + if (access_token_.isEmpty()) return false; + + std::shared_ptr search = std::make_shared(); + + search->id = id; + search->artist = artist; + search->title = title; + requests_search_.insert(id, search); + + const ParamList params = ParamList() << Param("q", QString(artist + " " + title)); + + QUrlQuery url_query; + for (const Param ¶m : params) { + url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + } + + QUrl url(kUrlSearch); + url.setQuery(url_query); + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + req.setRawHeader("Authorization", "Bearer " + access_token_.toUtf8()); + QNetworkReply *reply = network_->get(req); + connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id); }); + + //qLog(Debug) << "GeniusLyrics: Sending request for" << url; + + return true; + +} + +void GeniusLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); } + +void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id) { + + reply->deleteLater(); + + if (!requests_search_.contains(id)) return; + std::shared_ptr search = requests_search_.value(id); + + QJsonObject json_obj = ExtractJsonObj(reply); + if (json_obj.isEmpty()) { + EndSearch(search); + return; + } + + if (!json_obj.contains("meta")) { + Error("Json reply is missing meta.", json_obj); + EndSearch(search); + return; + } + if (!json_obj["meta"].isObject()) { + Error("Json reply meta is not an object.", json_obj); + EndSearch(search); + return; + } + QJsonObject obj_meta = json_obj["meta"].toObject(); + if (!obj_meta.contains("status")) { + Error("Json reply meta object is missing status.", obj_meta); + EndSearch(search); + return; + } + int status = obj_meta["status"].toInt(); + if (status != 200) { + if (obj_meta.contains("message")) { + Error(QString("Received error %1: %2.").arg(status).arg(obj_meta["message"].toString())); + } + else { + Error(QString("Received error %1.").arg(status)); + } + EndSearch(search); + return; + } + + if (!json_obj.contains("response")) { + Error("Json reply is missing response.", json_obj); + EndSearch(search); + return; + } + if (!json_obj["response"].isObject()) { + Error("Json response is not an object.", json_obj); + EndSearch(search); + return; + } + QJsonObject obj_response = json_obj["response"].toObject(); + if (!obj_response.contains("hits")) { + Error("Json response is missing hits.", obj_response); + EndSearch(search); + return; + } + if (!obj_response["hits"].isArray()) { + Error("Json hits is not an array.", obj_response); + EndSearch(search); + return; + } + QJsonArray array_hits = obj_response["hits"].toArray(); + + for (QJsonValue value_hit : array_hits) { + if (!value_hit.isObject()) { + continue; + } + QJsonObject obj_hit = value_hit.toObject(); + if (!obj_hit.contains("result")) { + continue; + } + if (!obj_hit["result"].isObject()) { + continue; + } + QJsonObject obj_result = obj_hit["result"].toObject(); + if (!obj_result.contains("title") || !obj_result.contains("primary_artist") || !obj_result.contains("url") || !obj_result["primary_artist"].isObject()) { + Error("Missing one or more values in result object", obj_result); + continue; + } + QJsonObject primary_artist = obj_result["primary_artist"].toObject(); + if (!primary_artist.contains("name")) continue; + + QString artist = primary_artist["name"].toString(); + QString title = obj_result["title"].toString(); + + // Ignore results where both the artist and title don't match. + if (artist.toLower() != search->artist.toLower() && title.toLower() != search->title.toLower()) continue; + + QUrl url(obj_result["url"].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); + + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QNetworkReply *new_reply = network_->get(req); + connect(new_reply, &QNetworkReply::finished, [=] { HandleLyricReply(new_reply, search->id, url); }); + + } + + EndSearch(search); + +} + +void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url) { + + reply->deleteLater(); + + if (!requests_search_.contains(search_id)) return; + std::shared_ptr 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(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + EndSearch(search, lyric); + return; + } + else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); + EndSearch(search, lyric); + return; + } + + QByteArray data = reply->readAll(); + if (data.isEmpty()) { + Error("Empty reply received from server."); + EndSearch(search, lyric); + return; + } + + QTextCodec *codec = QTextCodec::codecForName("utf-8"); + if (!codec) { + EndSearch(search, lyric); + return; + } + QString content = codec->toUnicode(data); + + // Extract the lyrics from HTML. + + QString tag_begin = "
    "; + QString tag_end = "
    "; + int begin_idx = content.indexOf(tag_begin); + QString lyrics; + if (begin_idx > 0) { + begin_idx += tag_begin.length(); + int end_idx = content.indexOf(tag_end, begin_idx); + lyrics = content.mid(begin_idx, end_idx - begin_idx); + lyrics = lyrics.remove(QRegExp("<[^>]*>")); + lyrics = lyrics.trimmed(); + } + + if (!lyrics.isEmpty()) { + LyricsSearchResult result; + result.artist = lyric.artist; + result.title = lyric.title; + result.lyrics = lyrics; + search->results.append(result); + } + + EndSearch(search, lyric); + +} + +void GeniusLyricsProvider::AuthError(const QString &error, const QVariant &debug) { + + if (!error.isEmpty()) login_errors_ << error; + + for (const QString &e : login_errors_) { + Error(0, e); + } + if (debug.isValid()) qLog(Debug) << debug; + + emit AuthenticationFailure(login_errors_); + 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(std::shared_ptr 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); + if (search->results.isEmpty()) { + qLog(Debug) << "GeniusLyrics: No lyrics for" << search->artist << search->title; + } + else { + qLog(Debug) << "GeniusLyrics: Got lyrics for" << search->artist << search->title; + } + emit SearchFinished(search->id, search->results); + } + +} diff --git a/src/lyrics/geniuslyricsprovider.h b/src/lyrics/geniuslyricsprovider.h new file mode 100644 index 000000000..45e0bf4f8 --- /dev/null +++ b/src/lyrics/geniuslyricsprovider.h @@ -0,0 +1,108 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 +#include +#include +#include +#include +#include +#include + +#include "jsonlyricsprovider.h" +#include "lyricsfetcher.h" + +class QNetworkAccessManager; +class QNetworkReply; + +class LocalRedirectServer; + +class GeniusLyricsProvider : public JsonLyricsProvider { + Q_OBJECT + + public: + explicit GeniusLyricsProvider(QObject *parent = nullptr); + + bool IsAuthenticated() { return !access_token_.isEmpty(); } + void Authenticate(); + void Deauthenticate() { access_token_.clear(); } + + bool StartSearch(const QString &artist, const QString &album, const QString &title, quint64 id); + void CancelSearch(const quint64 id); + + public: + struct GeniusLyricsLyricContext { + explicit GeniusLyricsLyricContext() {} + QString artist; + QString title; + QUrl url; + }; + struct GeniusLyricsSearchContext { + explicit GeniusLyricsSearchContext() : id(-1) {} + int id; + QString artist; + QString title; + QMap requests_lyric_; + LyricsSearchResults results; + }; + + private: + 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()); + void EndSearch(std::shared_ptr search, const GeniusLyricsLyricContext lyric = GeniusLyricsLyricContext()); + + private slots: + void HandleLoginSSLErrors(QList ssl_errors); + void RedirectArrived(); + void AccessTokenRequestFinished(QNetworkReply *reply); + void HandleSearchReply(QNetworkReply *reply, const quint64 id); + void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url); + + private: + static const char *kSettingsGroup; + static const char *kClientIDB64; + static const char *kClientSecretB64; + static const char *kOAuthAuthorizeUrl; + static const char *kOAuthAccessTokenUrl; + static const char *kOAuthRedirectUrl; + static const char *kUrlSearch; + + private: + QNetworkAccessManager *network_; + LocalRedirectServer *server_; + QString code_verifier_; + QString code_challenge_; + QString access_token_; + QStringList login_errors_; + QMap> requests_search_; + +}; + +#endif // GENIUSLYRICSPROVIDER_H diff --git a/src/lyrics/jsonlyricsprovider.cpp b/src/lyrics/jsonlyricsprovider.cpp index 26eb777b7..d5433d3b7 100644 --- a/src/lyrics/jsonlyricsprovider.cpp +++ b/src/lyrics/jsonlyricsprovider.cpp @@ -30,26 +30,27 @@ #include "lyricsprovider.h" #include "jsonlyricsprovider.h" -JsonLyricsProvider::JsonLyricsProvider(const QString &name, QObject *parent) : LyricsProvider(name, parent) {} +JsonLyricsProvider::JsonLyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent) : LyricsProvider(name, enabled, authentication_required, parent) {} -QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply, const quint64 id) { +QByteArray JsonLyricsProvider::ExtractData(QNetworkReply *reply) { - QString failure_reason; if (reply->error() != QNetworkReply::NoError) { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); if (reply->error() < 200) { - Error(id, failure_reason); - return QJsonObject(); + return QByteArray(); } } else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - failure_reason = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); } - QByteArray data = reply->readAll(); + return reply->readAll(); + +} + +QJsonObject JsonLyricsProvider::ExtractJsonObj(const QByteArray &data) { + if (data.isEmpty()) { - if (failure_reason.isEmpty()) failure_reason = "Empty reply received from server."; - Error(id, failure_reason); return QJsonObject(); } @@ -57,26 +58,32 @@ QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply, const quint QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { - Error(id, "Reply from server missing Json data."); + Error(QString("Failed to parse json data: %1").arg(error.errorString())); return QJsonObject(); } - if (json_doc.isNull() || json_doc.isEmpty()) { - Error(id, "Received empty Json document."); + if (json_doc.isEmpty()) { + Error("Received empty Json document.", data); return QJsonObject(); } if (!json_doc.isObject()) { - Error(id, "Json document is not an object."); + Error("Json document is not an object.", json_doc); return QJsonObject(); } QJsonObject json_obj = json_doc.object(); if (json_obj.isEmpty()) { - Error(id, "Received empty Json object."); + Error("Received empty Json object.", json_doc); return QJsonObject(); } return json_obj; } + +QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply) { + + return ExtractJsonObj(ExtractData(reply)); + +} diff --git a/src/lyrics/jsonlyricsprovider.h b/src/lyrics/jsonlyricsprovider.h index e7a40eec8..c2e708865 100644 --- a/src/lyrics/jsonlyricsprovider.h +++ b/src/lyrics/jsonlyricsprovider.h @@ -36,11 +36,14 @@ class JsonLyricsProvider : public LyricsProvider { Q_OBJECT public: - explicit JsonLyricsProvider(const QString &name, QObject *parent = nullptr); - QJsonObject ExtractJsonObj(QNetworkReply *reply, const quint64 id); + explicit JsonLyricsProvider(const QString &name, const bool enabled = true, const bool authentication_required = false, QObject *parent = nullptr); + + QByteArray ExtractData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(const QByteArray &data); + QJsonObject ExtractJsonObj(QNetworkReply *reply); private: - virtual void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()) = 0; + virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; }; diff --git a/src/lyrics/lololyricsprovider.cpp b/src/lyrics/lololyricsprovider.cpp index 1cb031cf2..823345a62 100644 --- a/src/lyrics/lololyricsprovider.cpp +++ b/src/lyrics/lololyricsprovider.cpp @@ -41,7 +41,7 @@ const char *LoloLyricsProvider::kUrlSearch = "http://api.lololyrics.com/0.5/getLyric"; -LoloLyricsProvider::LoloLyricsProvider(QObject *parent) : LyricsProvider("LoloLyrics", parent), network_(new NetworkAccessManager(this)) {} +LoloLyricsProvider::LoloLyricsProvider(QObject *parent) : LyricsProvider("LoloLyrics", true, false, parent), network_(new NetworkAccessManager(this)) {} bool LoloLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) { @@ -80,7 +80,8 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i if (reply->error() != QNetworkReply::NoError) { failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); if (reply->error() < 200) { - Error(id, failure_reason); + Error(failure_reason); + emit SearchFinished(id, LyricsSearchResults()); return; } } @@ -119,12 +120,6 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i else if (type == QXmlStreamReader::EndElement) { if (name == "result") { if (!result.lyrics.isEmpty()) { - if (result.artist.toLower() == artist.toLower()) - result.score += 1.0; - if (result.title.toLower() == title.toLower()) - result.score += 1.0; - if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength) - result.score += 1.0; results << result; } result = LyricsSearchResult(); @@ -140,10 +135,9 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i } -void LoloLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) { +void LoloLyricsProvider::Error(const QString &error, const QVariant &debug) { qLog(Error) << "LoloLyrics:" << error; if (debug.isValid()) qLog(Debug) << debug; - emit SearchFinished(id, LyricsSearchResults()); } diff --git a/src/lyrics/lololyricsprovider.h b/src/lyrics/lololyricsprovider.h index ec84542f6..3da7f9925 100644 --- a/src/lyrics/lololyricsprovider.h +++ b/src/lyrics/lololyricsprovider.h @@ -47,7 +47,7 @@ class LoloLyricsProvider : public LyricsProvider { private: static const char *kUrlSearch; QNetworkAccessManager *network_; - void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); }; diff --git a/src/lyrics/lyricsfetcher.cpp b/src/lyrics/lyricsfetcher.cpp index a77170d79..5bcda5688 100644 --- a/src/lyrics/lyricsfetcher.cpp +++ b/src/lyrics/lyricsfetcher.cpp @@ -29,7 +29,6 @@ #include "lyricsfetchersearch.h" const int LyricsFetcher::kMaxConcurrentRequests = 5; -const int LyricsFetcher::kGoodLyricsLength = 60; LyricsFetcher::LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent) : QObject(parent), @@ -104,8 +103,9 @@ void LyricsFetcher::StartRequests() { void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsSearchResults &results) { + if (!active_requests_.contains(request_id)) return; + LyricsFetcherSearch *search = active_requests_.take(request_id); - if (!search) return; search->deleteLater(); emit SearchFinished(request_id, results); @@ -113,8 +113,9 @@ void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsS void LyricsFetcher::SingleLyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics) { + if (!active_requests_.contains(request_id)) return; + LyricsFetcherSearch *search = active_requests_.take(request_id); - if (!search) return; search->deleteLater(); emit LyricsFetched(request_id, provider, lyrics); diff --git a/src/lyrics/lyricsfetcher.h b/src/lyrics/lyricsfetcher.h index 79744a6ad..59a0799c5 100644 --- a/src/lyrics/lyricsfetcher.h +++ b/src/lyrics/lyricsfetcher.h @@ -53,9 +53,9 @@ struct LyricsSearchResult { QString lyrics; float score; }; -Q_DECLARE_METATYPE(LyricsSearchResult) - typedef QList LyricsSearchResults; + +Q_DECLARE_METATYPE(LyricsSearchResult) Q_DECLARE_METATYPE(QList) class LyricsFetcher : public QObject { @@ -65,13 +65,13 @@ class LyricsFetcher : public QObject { explicit LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent = nullptr); virtual ~LyricsFetcher() {} - static const int kMaxConcurrentRequests; - static const int kGoodLyricsLength; - quint64 Search(const QString &artist, const QString &album, const QString &title); void Clear(); -signals: + private: + void AddRequest(const LyricsSearchRequest &req); + + signals: void LyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics); void SearchFinished(const quint64 request_id, const LyricsSearchResults &results); @@ -81,7 +81,7 @@ signals: void StartRequests(); private: - void AddRequest(const LyricsSearchRequest &req); + static const int kMaxConcurrentRequests; LyricsProviders *lyrics_providers_; quint64 next_id_; diff --git a/src/lyrics/lyricsfetchersearch.cpp b/src/lyrics/lyricsfetchersearch.cpp index 541380098..22b8a56b0 100644 --- a/src/lyrics/lyricsfetchersearch.cpp +++ b/src/lyrics/lyricsfetchersearch.cpp @@ -19,6 +19,8 @@ #include "config.h" +#include + #include #include #include @@ -30,10 +32,11 @@ #include "lyricsprovider.h" #include "lyricsproviders.h" -const int LyricsFetcherSearch::kSearchTimeoutMs = 6000; +const int LyricsFetcherSearch::kSearchTimeoutMs = 3000; +const int LyricsFetcherSearch::kGoodLyricsLength = 60; +const float LyricsFetcherSearch::kHighScore = 2.5; -LyricsFetcherSearch::LyricsFetcherSearch( - const LyricsSearchRequest &request, QObject *parent) +LyricsFetcherSearch::LyricsFetcherSearch(const LyricsSearchRequest &request, QObject *parent) : QObject(parent), request_(request), cancel_requested_(false) { @@ -53,7 +56,11 @@ void LyricsFetcherSearch::TerminateSearch() { void LyricsFetcherSearch::Start(LyricsProviders *lyrics_providers) { - for (LyricsProvider *provider : lyrics_providers->List()) { + QList lyrics_providers_sorted = lyrics_providers->List(); + std::stable_sort(lyrics_providers_sorted.begin(), lyrics_providers_sorted.end(), ProviderCompareOrder); + + for (LyricsProvider *provider : lyrics_providers_sorted) { + if (!provider->is_enabled() || !provider->IsAuthenticated()) continue; connect(provider, SIGNAL(SearchFinished(quint64, LyricsSearchResults)), SLOT(ProviderSearchFinished(quint64, LyricsSearchResults))); const int id = lyrics_providers->NextId(); const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id); @@ -70,13 +77,37 @@ void LyricsFetcherSearch::ProviderSearchFinished(const quint64 id, const LyricsS LyricsProvider *provider = pending_requests_.take(id); LyricsSearchResults results_copy(results); - for (int i = 0; i < results_copy.count(); ++i) { + float higest_score = 0.0; + for (int i = 0 ; i < results_copy.count() ; ++i) { results_copy[i].provider = provider->name(); + results_copy[i].score = 0.0; + if (results_copy[i].artist.toLower() == request_.artist.toLower()) { + results_copy[i].score += 0.5; + } + if (results_copy[i].album.toLower() == request_.album.toLower()) { + results_copy[i].score += 0.5; + } + if (results_copy[i].title.toLower() == request_.title.toLower()) { + results_copy[i].score += 0.5; + } + if (results_copy[i].artist.toLower() != request_.artist.toLower() && results_copy[i].title.toLower() != request_.title.toLower()) { + results_copy[i].score -= 1.5; + } + if (results_copy[i].lyrics.length() > kGoodLyricsLength) results_copy[i].score += 1.0; + if (results_copy[i].score > higest_score) higest_score = results_copy[i].score; } results_.append(results_copy); + std::stable_sort(results_.begin(), results_.end(), LyricsSearchResultCompareScore); if (!pending_requests_.isEmpty()) { + if (!results_.isEmpty() && higest_score >= kHighScore) { // Highest score, no need to wait for other providers. + qLog(Debug) << "Got lyrics with high score from" << results_.last().provider << "for" << request_.artist << request_.title << "score" << results_.last().score << "finishing search."; + TerminateSearch(); + } + else { + return; + } return; } @@ -89,14 +120,10 @@ void LyricsFetcherSearch::AllProvidersFinished() { if (cancel_requested_) return; if (!results_.isEmpty()) { - LyricsSearchResult result_use; - result_use.score = 0.0; - for (LyricsSearchResult result : results_) { - if (result_use.lyrics.isEmpty() || result.score > result_use.score) result_use = result; - } - qLog(Debug) << "Using lyrics from" << result_use.provider << "for" << request_.artist << request_.title << "with score" << result_use.score; - emit LyricsFetched(request_.id, result_use.provider, result_use.lyrics); + qLog(Debug) << "Using lyrics from" << results_.last().provider << "for" << request_.artist << request_.title << "with score" << results_.last().score; + emit LyricsFetched(request_.id, results_.last().provider, results_.last().lyrics); } + emit SearchFinished(request_.id, results_); } @@ -111,3 +138,11 @@ void LyricsFetcherSearch::Cancel() { } +bool LyricsFetcherSearch::ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b) { + return a->order() < b->order(); +} + +bool LyricsFetcherSearch::LyricsSearchResultCompareScore(const LyricsSearchResult &a, const LyricsSearchResult &b) { + return a.score < b.score; +} + diff --git a/src/lyrics/lyricsfetchersearch.h b/src/lyrics/lyricsfetchersearch.h index f2f551bd0..02f4c53dd 100644 --- a/src/lyrics/lyricsfetchersearch.h +++ b/src/lyrics/lyricsfetchersearch.h @@ -51,11 +51,13 @@ class LyricsFetcherSearch : public QObject { private: void AllProvidersFinished(); - - void SendBestImage(); + static bool ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b); + static bool LyricsSearchResultCompareScore(const LyricsSearchResult &a, const LyricsSearchResult &b); private: static const int kSearchTimeoutMs; + static const int kGoodLyricsLength; + static const float kHighScore; LyricsSearchRequest request_; LyricsSearchResults results_; diff --git a/src/lyrics/lyricsprovider.cpp b/src/lyrics/lyricsprovider.cpp index a6bf54acd..3d09056ed 100644 --- a/src/lyrics/lyricsprovider.cpp +++ b/src/lyrics/lyricsprovider.cpp @@ -24,5 +24,5 @@ #include "lyricsprovider.h" -LyricsProvider::LyricsProvider(const QString &name, QObject *parent) - : QObject(parent), name_(name) {} +LyricsProvider::LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent) + : QObject(parent), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required) {} diff --git a/src/lyrics/lyricsprovider.h b/src/lyrics/lyricsprovider.h index 0505bb28d..c181eb45d 100644 --- a/src/lyrics/lyricsprovider.h +++ b/src/lyrics/lyricsprovider.h @@ -34,22 +34,36 @@ class LyricsProvider : public QObject { Q_OBJECT public: - explicit LyricsProvider(const QString &name, QObject *parent); + explicit LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent); typedef QPair Param; typedef QList ParamList; QString name() const { return name_; } + bool is_enabled() const { return enabled_; } + int order() const { return order_; } + + void set_enabled(const bool enabled) { enabled_ = enabled; } + void set_order(const int order) { order_ = order; } virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) = 0; virtual void CancelSearch(const quint64 id) { Q_UNUSED(id); } + virtual bool AuthenticationRequired() { return authentication_required_; } + virtual void Authenticate() {} + virtual bool IsAuthenticated() { return !authentication_required_; } + virtual void Deauthenticate() {} signals: + void AuthenticationComplete(bool, QStringList = QStringList()); + void AuthenticationSuccess(); + void AuthenticationFailure(QStringList); void SearchFinished(const quint64 id, const LyricsSearchResults &results); private: QString name_; - + bool enabled_; + int order_; + bool authentication_required_; }; #endif // LYRICSPROVIDER_H diff --git a/src/lyrics/lyricsproviders.cpp b/src/lyrics/lyricsproviders.cpp index 888b88d35..b9aaecba2 100644 --- a/src/lyrics/lyricsproviders.cpp +++ b/src/lyrics/lyricsproviders.cpp @@ -21,13 +21,20 @@ #include #include +#include +#include +#include #include +#include +#include #include #include "core/logging.h" #include "lyricsprovider.h" #include "lyricsproviders.h" +#include "settings/lyricssettingspage.h" + LyricsProviders::LyricsProviders(QObject *parent) : QObject(parent) {} LyricsProviders::~LyricsProviders() { @@ -38,6 +45,48 @@ LyricsProviders::~LyricsProviders() { } +void LyricsProviders::ReloadSettings() { + + QStringList all_providers; + for (LyricsProvider *provider : lyrics_providers_.keys()) { + if (!provider->is_enabled()) continue; + all_providers << provider->name(); + } + + QSettings s; + s.beginGroup(LyricsSettingsPage::kSettingsGroup); + QStringList providers_enabled = s.value("providers", all_providers).toStringList(); + s.endGroup(); + + int i = 0; + QList providers; + for (const QString &name : providers_enabled) { + LyricsProvider *provider = ProviderByName(name); + if (provider) { + provider->set_enabled(true); + provider->set_order(++i); + providers << provider; + } + } + + for (LyricsProvider *provider : lyrics_providers_.keys()) { + if (!providers.contains(provider)) { + provider->set_enabled(false); + provider->set_order(++i); + } + } + +} + +LyricsProvider *LyricsProviders::ProviderByName(const QString &name) const { + + for (LyricsProvider *provider : lyrics_providers_.keys()) { + if (provider->name() == name) return provider; + } + return nullptr; + +} + void LyricsProviders::AddProvider(LyricsProvider *provider) { { diff --git a/src/lyrics/lyricsproviders.h b/src/lyrics/lyricsproviders.h index 8ee945df2..de12ce3cc 100644 --- a/src/lyrics/lyricsproviders.h +++ b/src/lyrics/lyricsproviders.h @@ -39,6 +39,9 @@ class LyricsProviders : public QObject { explicit LyricsProviders(QObject *parent = nullptr); ~LyricsProviders(); + void ReloadSettings(); + LyricsProvider *ProviderByName(const QString &name) const; + void AddProvider(LyricsProvider *provider); void RemoveProvider(LyricsProvider *provider); QList List() const { return lyrics_providers_.keys(); } @@ -51,7 +54,8 @@ class LyricsProviders : public QObject { private: Q_DISABLE_COPY(LyricsProviders) - QMap lyrics_providers_; + QMap lyrics_providers_; + QList ordered_providers_; QMutex mutex_; QAtomicInt next_id_; diff --git a/src/lyrics/musixmatchlyricsprovider.cpp b/src/lyrics/musixmatchlyricsprovider.cpp new file mode 100644 index 000000000..72d69fb9c --- /dev/null +++ b/src/lyrics/musixmatchlyricsprovider.cpp @@ -0,0 +1,214 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 "core/logging.h" +#include "core/utilities.h" +#include "core/network.h" +#include "internet/localredirectserver.h" +#include "jsonlyricsprovider.h" +#include "lyricsfetcher.h" +#include "lyricsprovider.h" +#include "musixmatchlyricsprovider.h" + +const char *MusixmatchLyricsProvider::kSettingsGroup = "MusixmatchLyrics"; + +MusixmatchLyricsProvider::MusixmatchLyricsProvider(QObject *parent) : JsonLyricsProvider("Musixmatch", true, false, parent), network_(new NetworkAccessManager(this)) {} + +bool MusixmatchLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) { + + QString artist_stripped = artist; + QString title_stripped = title; + + artist_stripped = artist_stripped.replace('/', '-'); + artist_stripped = artist_stripped.remove(QRegExp("[^A-Za-z0-9\\- ]")); + artist_stripped = artist_stripped.simplified(); + artist_stripped = artist_stripped.replace(' ', '-'); + artist_stripped = artist_stripped.replace(QRegExp("(-)\\1+"), "-"); + artist_stripped = artist_stripped.toLower(); + + title_stripped = title_stripped.replace('/', '-'); + title_stripped = title_stripped.remove(QRegExp("[^a-zA-Z0-9\\- ]")); + title_stripped = title_stripped.simplified(); + title_stripped = title_stripped.replace(' ', '-').toLower(); + title_stripped = title_stripped.replace(QRegExp("(-)\\1+"), "-"); + title_stripped = title_stripped.toLower(); + + if (artist_stripped.isEmpty() || title_stripped.isEmpty()) return false; + + QUrl url(QString("https://www.musixmatch.com/lyrics/%1/%2").arg(artist_stripped).arg(title_stripped)); + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QNetworkReply *reply = network_->get(req); + connect(reply, &QNetworkReply::finished, [=] { HandleSearchReply(reply, id, artist, album, title); }); + + qLog(Debug) << "MusixmatchLyrics: Sending request for" << artist_stripped << title_stripped << url; + + return true; + +} + +void MusixmatchLyricsProvider::CancelSearch(const quint64 id) { Q_UNUSED(id); } + +void MusixmatchLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &album, const QString &title) { + + Q_UNUSED(album); + + reply->deleteLater(); + + LyricsSearchResults results; + + if (reply->error() != QNetworkReply::NoError) { + if (reply->error() < 200) { + Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); + emit SearchFinished(id, results); + return; + } + } + else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { + Error(QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt())); + emit SearchFinished(id, results); + return; + } + + QByteArray data = reply->readAll(); + if (data.isEmpty()) { + Error("Empty reply received from server."); + emit SearchFinished(id, results); + return; + } + + QTextCodec *codec = QTextCodec::codecForName("utf-8"); + if (!codec) { + emit SearchFinished(id, results); + return; + } + QString content = codec->toUnicode(data); + + QString data_begin = "var __mxmState = "; + QString data_end = ";"; + int begin_idx = content.indexOf(data_begin); + QString content_json; + if (begin_idx > 0) { + begin_idx += data_begin.length(); + int end_idx = content.indexOf(data_end, begin_idx); + if (end_idx > begin_idx) { + content_json = content.mid(begin_idx, end_idx - begin_idx); + } + } + + if (content_json.isEmpty()) { + emit SearchFinished(id, results); + return; + } + + if (content_json.contains(QRegExp("<[^>]*>"))) { // Make sure it's not HTML code. + emit SearchFinished(id, results); + return; + } + + QJsonObject json_obj = ExtractJsonObj(content_json.toUtf8()); + if (json_obj.isEmpty()) { + emit SearchFinished(id, results); + return; + } + + if (!json_obj.contains("page") || !json_obj["page"].isObject()) { + Error("Json reply is missing page.", json_obj); + emit SearchFinished(id, results); + return; + } + json_obj = json_obj["page"].toObject(); + + + if (!json_obj.contains("track") || !json_obj["track"].isObject()) { + Error("Json reply is missing track.", json_obj); + emit SearchFinished(id, results); + return; + } + QJsonObject obj_track = json_obj["track"].toObject(); + + if (!obj_track.contains("artistName") || !obj_track.contains("albumName") || !obj_track.contains("name")) { + Error("Json track is missing artistName, albumName or name.", json_obj); + emit SearchFinished(id, results); + return; + } + + if (!json_obj.contains("lyrics") || !json_obj["lyrics"].isObject()) { + Error("Json reply is missing lyrics.", json_obj); + emit SearchFinished(id, results); + return; + } + QJsonObject obj_lyrics = json_obj["lyrics"].toObject(); + + if (!obj_lyrics.contains("lyrics") || !obj_lyrics["lyrics"].isObject()) { + Error("Json reply is missing lyrics.", obj_lyrics); + emit SearchFinished(id, results); + return; + } + obj_lyrics = obj_lyrics["lyrics"].toObject(); + + if (!obj_lyrics.contains("body")) { + Error("Json lyrics is missing body.", obj_lyrics); + emit SearchFinished(id, results); + } + + LyricsSearchResult result; + result.artist = obj_track["artistName"].toString(); + result.album = obj_track["albumName"].toString(); + result.title = obj_track["name"].toString(); + result.lyrics = obj_lyrics["body"].toString(); + + if (!result.lyrics.isEmpty() && (artist.toLower() == result.artist.toLower() || title.toLower() == result.title.toLower())) { + results.append(result); + } + + if (results.isEmpty()) { + qLog(Debug) << "MusixmatchLyrics: No lyrics for" << artist << title; + } + else { + qLog(Debug) << "MusixmatchLyrics: Got lyrics for" << artist << title; + } + + emit SearchFinished(id, results); + +} + +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 new file mode 100644 index 000000000..8fccac544 --- /dev/null +++ b/src/lyrics/musixmatchlyricsprovider.h @@ -0,0 +1,64 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 MUSIXMATCHLYRICSPROVIDER_H +#define MUSIXMATCHLYRICSPROVIDER_H + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsonlyricsprovider.h" +#include "lyricsfetcher.h" + +class QNetworkAccessManager; +class QNetworkReply; + +class MusixmatchLyricsProvider : public JsonLyricsProvider { + Q_OBJECT + + public: + explicit MusixmatchLyricsProvider(QObject *parent = nullptr); + + bool StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id); + void CancelSearch(const quint64 id); + + private: + void Error(const QString &error, const QVariant &debug = QVariant()); + + private slots: + void HandleSearchReply(QNetworkReply *reply, const quint64 id, const QString &artist, const QString &album, const QString &title); + + private: + static const char *kSettingsGroup; + + private: + QNetworkAccessManager *network_; + +}; + +#endif // MUSIXMATCHLYRICSPROVIDER_H diff --git a/src/lyrics/ovhlyricsprovider.cpp b/src/lyrics/ovhlyricsprovider.cpp index 1081caa8b..c219338c3 100644 --- a/src/lyrics/ovhlyricsprovider.cpp +++ b/src/lyrics/ovhlyricsprovider.cpp @@ -38,7 +38,7 @@ const char *OVHLyricsProvider::kUrlSearch = "https://api.lyrics.ovh/v1/"; -OVHLyricsProvider::OVHLyricsProvider(QObject *parent) : JsonLyricsProvider("Lyrics.ovh", parent), network_(new NetworkAccessManager(this)) {} +OVHLyricsProvider::OVHLyricsProvider(QObject *parent) : JsonLyricsProvider("Lyrics.ovh", true, false, parent), network_(new NetworkAccessManager(this)) {} bool OVHLyricsProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const quint64 id) { @@ -62,14 +62,14 @@ void OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id reply->deleteLater(); - QJsonObject json_obj = ExtractJsonObj(reply, id); + QJsonObject json_obj = ExtractJsonObj(reply); if (json_obj.isEmpty()) { emit SearchFinished(id, LyricsSearchResults()); return; } if (json_obj.contains("error")) { - Error(id, json_obj["error"].toString()); + Error(json_obj["error"].toString()); qLog(Debug) << "OVHLyrics: No lyrics for" << artist << title; emit SearchFinished(id, LyricsSearchResults()); return; @@ -82,19 +82,17 @@ void OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id LyricsSearchResult result; result.lyrics = json_obj["lyrics"].toString(); - if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength) - result.score += 1.0; qLog(Debug) << "OVHLyrics: Got lyrics for" << artist << title; + emit SearchFinished(id, LyricsSearchResults() << result); } -void OVHLyricsProvider::Error(const quint64 id, const QString &error, const QVariant &debug) { +void OVHLyricsProvider::Error(const QString &error, const QVariant &debug) { qLog(Error) << "OVHLyrics:" << error; if (debug.isValid()) qLog(Debug) << debug; - emit SearchFinished(id, LyricsSearchResults()); } diff --git a/src/lyrics/ovhlyricsprovider.h b/src/lyrics/ovhlyricsprovider.h index 9c5fe9220..716b0fbfd 100644 --- a/src/lyrics/ovhlyricsprovider.h +++ b/src/lyrics/ovhlyricsprovider.h @@ -47,7 +47,7 @@ class OVHLyricsProvider : public JsonLyricsProvider { private: static const char *kUrlSearch; QNetworkAccessManager *network_; - void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()); + void Error(const QString &error, const QVariant &debug = QVariant()); }; diff --git a/src/scrobbler/listenbrainzscrobbler.cpp b/src/scrobbler/listenbrainzscrobbler.cpp index a99ec77b3..35124e547 100644 --- a/src/scrobbler/listenbrainzscrobbler.cpp +++ b/src/scrobbler/listenbrainzscrobbler.cpp @@ -122,7 +122,8 @@ void ListenBrainzScrobbler::Logout() { void ListenBrainzScrobbler::Authenticate(const bool https) { if (!server_) { - server_ = new LocalRedirectServer(https, this); + server_ = new LocalRedirectServer(this); + server_->set_https(https); if (!server_->Listen()) { AuthError(server_->error()); delete server_; diff --git a/src/scrobbler/scrobblingapi20.cpp b/src/scrobbler/scrobblingapi20.cpp index cc32a2a59..aefebab09 100644 --- a/src/scrobbler/scrobblingapi20.cpp +++ b/src/scrobbler/scrobblingapi20.cpp @@ -126,7 +126,8 @@ void ScrobblingAPI20::Logout() { void ScrobblingAPI20::Authenticate(const bool https) { if (!server_) { - server_ = new LocalRedirectServer(https, this); + server_ = new LocalRedirectServer(this); + server_->set_https(https); if (!server_->Listen()) { AuthError(server_->error()); delete server_; diff --git a/src/settings/lyricssettingspage.cpp b/src/settings/lyricssettingspage.cpp new file mode 100644 index 000000000..fb622e621 --- /dev/null +++ b/src/settings/lyricssettingspage.cpp @@ -0,0 +1,251 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 "settingsdialog.h" +#include "lyricssettingspage.h" +#include "ui_lyricssettingspage.h" +#include "core/application.h" +#include "core/iconloader.h" +#include "core/logging.h" +#include "lyrics/lyricsproviders.h" +#include "lyrics/geniuslyricsprovider.h" +#include "widgets/loginstatewidget.h" + +const char *LyricsSettingsPage::kSettingsGroup = "Lyrics"; + +LyricsSettingsPage::LyricsSettingsPage(SettingsDialog *parent) : SettingsPage(parent), ui_(new Ui::LyricsSettingsPage), provider_selected_(false) { + + ui_->setupUi(this); + setWindowIcon(IconLoader::Load("view-media-lyrics")); + + connect(ui_->providers_up, SIGNAL(clicked()), SLOT(ProvidersMoveUp())); + connect(ui_->providers_down, SIGNAL(clicked()), SLOT(ProvidersMoveDown())); + connect(ui_->providers, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), SLOT(CurrentItemChanged(QListWidgetItem*, QListWidgetItem*))); + connect(ui_->providers, SIGNAL(itemSelectionChanged()), SLOT(ItemSelectionChanged())); + connect(ui_->providers, SIGNAL(itemChanged(QListWidgetItem*)), SLOT(ItemChanged(QListWidgetItem*))); + + connect(ui_->button_authenticate, SIGNAL(clicked()), SLOT(AuthenticateClicked())); + connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); + + NoProviderSelected(); + DisableAuthentication(); + + dialog()->installEventFilter(this); + +} + +LyricsSettingsPage::~LyricsSettingsPage() { delete ui_; } + +void LyricsSettingsPage::Load() { + + ui_->providers->clear(); + + QList lyrics_providers_sorted = dialog()->app()->lyrics_providers()->List(); + std::stable_sort(lyrics_providers_sorted.begin(), lyrics_providers_sorted.end(), ProviderCompareOrder); + + for (LyricsProvider *provider : lyrics_providers_sorted) { + QListWidgetItem *item = new QListWidgetItem(ui_->providers); + item->setText(provider->name()); + item->setCheckState(provider->is_enabled() ? Qt::Checked : Qt::Unchecked); + item->setForeground(provider->is_enabled() ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text)); + } + +} + +void LyricsSettingsPage::Save() { + + QStringList providers; + for (int i = 0 ; i < ui_->providers->count() ; ++i) { + const QListWidgetItem *item = ui_->providers->item(i); + if (item->checkState() == Qt::Checked) providers << item->text(); + } + + qLog(Debug) << providers; + + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("providers", providers); + s.endGroup(); + +} + +void LyricsSettingsPage::CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous) { + + if (item_previous) { + LyricsProvider *provider = dialog()->app()->lyrics_providers()->ProviderByName(item_previous->text()); + if (provider && provider->AuthenticationRequired()) DisconnectAuthentication(provider); + } + + if (item_current) { + const int row = ui_->providers->row(item_current); + ui_->providers_up->setEnabled(row != 0); + ui_->providers_down->setEnabled(row != ui_->providers->count() - 1); + LyricsProvider *provider = dialog()->app()->lyrics_providers()->ProviderByName(item_current->text()); + if (provider && provider->AuthenticationRequired()) { + ui_->login_state->SetLoggedIn(provider->IsAuthenticated() ? LoginStateWidget::LoggedIn : LoginStateWidget::LoggedOut); + ui_->button_authenticate->setEnabled(true); + ui_->button_authenticate->show(); + ui_->login_state->show(); + ui_->label_auth_info->setText(QString("%1 needs authentication.").arg(provider->name())); + } + else { + DisableAuthentication(); + ui_->label_auth_info->setText(QString("%1 does not need authentication.").arg(provider->name())); + } + provider_selected_ = true; + } + else { + DisableAuthentication(); + NoProviderSelected(); + ui_->providers_up->setEnabled(false); + ui_->providers_down->setEnabled(false); + provider_selected_ = false; + } + +} + +void LyricsSettingsPage::ItemSelectionChanged() { + + if (ui_->providers->selectedItems().count() == 0) { + DisableAuthentication(); + NoProviderSelected(); + ui_->providers_up->setEnabled(false); + ui_->providers_down->setEnabled(false); + provider_selected_ = false; + } + else { + if (ui_->providers->currentItem() && !provider_selected_) { + CurrentItemChanged(ui_->providers->currentItem(), nullptr); + } + } + +} + +void LyricsSettingsPage::ProvidersMoveUp() { ProvidersMove(-1); } + +void LyricsSettingsPage::ProvidersMoveDown() { ProvidersMove(+1); } + +void LyricsSettingsPage::ProvidersMove(const int d) { + + const int row = ui_->providers->currentRow(); + QListWidgetItem *item = ui_->providers->takeItem(row); + ui_->providers->insertItem(row + d, item); + ui_->providers->setCurrentRow(row + d); + +} + +void LyricsSettingsPage::ItemChanged(QListWidgetItem *item) { + + item->setForeground((item->checkState() == Qt::Checked) ? palette().color(QPalette::Active, QPalette::Text) : palette().color(QPalette::Disabled, QPalette::Text)); + +} + +void LyricsSettingsPage::NoProviderSelected() { + ui_->label_auth_info->setText(tr("No provider selected.")); +} + +void LyricsSettingsPage::DisableAuthentication() { + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); + ui_->button_authenticate->setEnabled(false); + ui_->login_state->hide(); + ui_->button_authenticate->hide(); + +} + +void LyricsSettingsPage::DisconnectAuthentication(LyricsProvider *provider) { + + disconnect(provider, SIGNAL(AuthenticationFailure(QStringList)), this, SLOT(AuthenticationFailure(QStringList))); + disconnect(provider, SIGNAL(AuthenticationSuccess()), this, SLOT(AuthenticationSuccess())); + +} + +void LyricsSettingsPage::AuthenticateClicked() { + + if (!ui_->providers->currentItem()) return; + LyricsProvider *provider = dialog()->app()->lyrics_providers()->ProviderByName(ui_->providers->currentItem()->text()); + if (!provider) return; + ui_->button_authenticate->setEnabled(false); + connect(provider, SIGNAL(AuthenticationFailure(QStringList)), this, SLOT(AuthenticationFailure(QStringList))); + connect(provider, SIGNAL(AuthenticationSuccess()), this, SLOT(AuthenticationSuccess())); + provider->Authenticate(); + +} + +void LyricsSettingsPage::LogoutClicked() { + + if (!ui_->providers->currentItem()) return; + LyricsProvider *provider = dialog()->app()->lyrics_providers()->ProviderByName(ui_->providers->currentItem()->text()); + if (!provider) return; + provider->Deauthenticate(); + + ui_->button_authenticate->setEnabled(true); + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); + +} + +void LyricsSettingsPage::AuthenticationSuccess() { + + LyricsProvider *provider = qobject_cast(sender()); + if (!provider) return; + DisconnectAuthentication(provider); + + if (!this->isVisible() || !ui_->providers->currentItem() || ui_->providers->currentItem()->text() != provider->name()) return; + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); + ui_->button_authenticate->setEnabled(true); + +} + +void LyricsSettingsPage::AuthenticationFailure(const QStringList &errors) { + + LyricsProvider *provider = qobject_cast(sender()); + if (!provider) return; + DisconnectAuthentication(provider); + + if (!this->isVisible() || !ui_->providers->currentItem() || ui_->providers->currentItem()->text() != provider->name()) return; + + QMessageBox::warning(this, tr("Authentication failed"), errors.join("\n")); + + ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); + ui_->button_authenticate->setEnabled(true); + +} + +bool LyricsSettingsPage::ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b) { + return a->order() < b->order(); +} diff --git a/src/settings/lyricssettingspage.h b/src/settings/lyricssettingspage.h new file mode 100644 index 000000000..6813ce421 --- /dev/null +++ b/src/settings/lyricssettingspage.h @@ -0,0 +1,72 @@ +/* + * Strawberry Music Player + * Copyright 2020, 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 LYRICSSETTINGSPAGE_H +#define LYRICSSETTINGSPAGE_H + +#include "config.h" + +#include +#include +#include + +#include "settings/settingspage.h" + +class QListWidgetItem; + +class LyricsProvider; +class SettingsDialog; +class Ui_LyricsSettingsPage; + +class LyricsSettingsPage : public SettingsPage { + Q_OBJECT + + public: + explicit LyricsSettingsPage(SettingsDialog *parent = nullptr); + ~LyricsSettingsPage(); + + static const char *kSettingsGroup; + + void Load(); + void Save(); + + private: + void NoProviderSelected(); + void ProvidersMove(const int d); + void DisableAuthentication(); + void DisconnectAuthentication(LyricsProvider *provider); + static bool ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b); + + private slots: + void CurrentItemChanged(QListWidgetItem *item_current, QListWidgetItem *item_previous); + void ItemSelectionChanged(); + void ItemChanged(QListWidgetItem *item); + void ProvidersMoveUp(); + void ProvidersMoveDown(); + void AuthenticateClicked(); + void LogoutClicked(); + void AuthenticationSuccess(); + void AuthenticationFailure(const QStringList &errors); + + private: + Ui_LyricsSettingsPage *ui_; + bool provider_selected_; +}; + +#endif // LYRICSSETTINGSPAGE_H diff --git a/src/settings/lyricssettingspage.ui b/src/settings/lyricssettingspage.ui new file mode 100644 index 000000000..f0fba9456 --- /dev/null +++ b/src/settings/lyricssettingspage.ui @@ -0,0 +1,166 @@ + + + LyricsSettingsPage + + + + 0 + 0 + 460 + 600 + + + + Lyrics + + + + + + + 0 + 0 + + + + Lyrics providers + + + + + + Choose the providers you want to use when searching for lyrics. + + + true + + + + + + + + + + + + + + false + + + Move up + + + + + + + false + + + Move down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + 0 + + + + Authentication + + + + + + + + + true + + + + + + + + + + + + Authenticate + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 30 + + + + + + + + + LoginStateWidget + QWidget +
    widgets/loginstatewidget.h
    + 1 +
    +
    + + + + + +
    diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index d43db82ad..33d87073c 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -56,17 +56,18 @@ #include "settingsdialog.h" #include "settingspage.h" -#include "appearancesettingspage.h" -#include "backendsettingspage.h" #include "behavioursettingspage.h" -#include "contextsettingspage.h" #include "collectionsettingspage.h" -#include "notificationssettingspage.h" +#include "backendsettingspage.h" #include "playlistsettingspage.h" +#include "appearancesettingspage.h" +#include "contextsettingspage.h" +#include "scrobblersettingspage.h" +#include "notificationssettingspage.h" #include "shortcutssettingspage.h" #include "transcodersettingspage.h" #include "networkproxysettingspage.h" -#include "scrobblersettingspage.h" +#include "lyricssettingspage.h" #ifdef HAVE_MOODBAR # include "moodbarsettingspage.h" #endif @@ -138,6 +139,7 @@ SettingsDialog::SettingsDialog(Application *app, QMainWindow *mainwindow, QWidge QTreeWidgetItem *iface = AddCategory(tr("User interface")); AddPage(Page_Appearance, new AppearanceSettingsPage(this), iface); AddPage(Page_Context, new ContextSettingsPage(this), iface); + AddPage(Page_Lyrics, new LyricsSettingsPage(this), iface); AddPage(Page_Notifications, new NotificationsSettingsPage(this), iface); #ifdef HAVE_GLOBALSHORTCUTS diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 0b8575cca..718a07d6d 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -81,6 +81,7 @@ class SettingsDialog : public QDialog { Page_GlobalShortcuts, Page_Appearance, Page_Context, + Page_Lyrics, Page_Notifications, Page_Transcoding, Page_Proxy,