Add setting for lyric providers and add more providers

Fixes #335
This commit is contained in:
Jonas Kvinge 2020-05-08 18:35:36 +02:00
parent 6ef69f6b32
commit f44ce49ea7
39 changed files with 1834 additions and 105 deletions

View File

@ -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 * Advanced audio output and device configuration for bit-perfect playback on Linux
* Edit tags on music files * Edit tags on music files
* Fetch tags from MusicBrainz * 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/) * 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/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.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 * Support for multiple backends
* Audio analyzer * Audio analyzer
* Audio equalizer * Audio equalizer
* Transfer music to iPod, iPhone, MTP or mass-storage USB player * 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/) * 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. It has so far been tested to work on Linux, OpenBSD and Windows.

2
debian/control vendored
View File

@ -56,7 +56,7 @@ Description: Audio player and music collection organizer
- Edit tags on music files - Edit tags on music files
- Fetch tags from MusicBrainz - Fetch tags from MusicBrainz
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer - 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 - Support for multiple backends
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer

View File

@ -26,8 +26,8 @@
<li>Advanced audio output and device configuration for bit-perfect playback on Linux</li> <li>Advanced audio output and device configuration for bit-perfect playback on Linux</li>
<li>Edit tags on music files</li> <li>Edit tags on music files</li>
<li>Fetch tags from MusicBrainz</li> <li>Fetch tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz and Discogs</li> <li>Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal</li>
<li>Song lyrics from AudD, lyrics.ovh and lololyrics.com</li> <li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
<li>Support for multiple backends</li> <li>Support for multiple backends</li>
<li>Audio analyzer and equalizer</li> <li>Audio analyzer and equalizer</li>
<li>Transfer music to iPod, iPhone, MTP or mass-storage USB player</li> <li>Transfer music to iPod, iPhone, MTP or mass-storage USB player</li>

View File

@ -25,9 +25,9 @@ Features:
.br .br
- Fetch tags from MusicBrainz - Fetch tags from MusicBrainz
.br .br
- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer - Album cover art from Lastfm, Musicbrainz, Discogs, Deezer and Tidal
.br .br
- Song lyrics from AudD, lyrics.ovh and lololyrics.com - Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
.br .br
- Support for multiple backends - Support for multiple backends
.br .br

View File

@ -104,8 +104,8 @@ Features:
- Advanced audio output and device configuration for bit-perfect playback on Linux - Advanced audio output and device configuration for bit-perfect playback on Linux
- Edit tags on music files - Edit tags on music files
- Fetch tags from MusicBrainz - Fetch tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs and Deezer - Album cover art from Last.fm, Musicbrainz, Discogs, Deezer and Tidal
- 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 - Support for multiple backends
- Audio analyzer - Audio analyzer
- Audio equalizer - Audio equalizer

View File

@ -196,6 +196,9 @@ set(SOURCES
lyrics/auddlyricsprovider.cpp lyrics/auddlyricsprovider.cpp
lyrics/ovhlyricsprovider.cpp lyrics/ovhlyricsprovider.cpp
lyrics/lololyricsprovider.cpp lyrics/lololyricsprovider.cpp
lyrics/geniuslyricsprovider.cpp
lyrics/musixmatchlyricsprovider.cpp
lyrics/chartlyricsprovider.cpp
settings/settingsdialog.cpp settings/settingsdialog.cpp
settings/settingspage.cpp settings/settingspage.cpp
@ -208,6 +211,7 @@ set(SOURCES
settings/appearancesettingspage.cpp settings/appearancesettingspage.cpp
settings/notificationssettingspage.cpp settings/notificationssettingspage.cpp
settings/scrobblersettingspage.cpp settings/scrobblersettingspage.cpp
settings/lyricssettingspage.cpp
dialogs/about.cpp dialogs/about.cpp
dialogs/console.cpp dialogs/console.cpp
@ -384,6 +388,9 @@ set(HEADERS
lyrics/auddlyricsprovider.h lyrics/auddlyricsprovider.h
lyrics/ovhlyricsprovider.h lyrics/ovhlyricsprovider.h
lyrics/lololyricsprovider.h lyrics/lololyricsprovider.h
lyrics/geniuslyricsprovider.h
lyrics/musixmatchlyricsprovider.h
lyrics/chartlyricsprovider.h
settings/settingsdialog.h settings/settingsdialog.h
settings/settingspage.h settings/settingspage.h
@ -396,6 +403,7 @@ set(HEADERS
settings/appearancesettingspage.h settings/appearancesettingspage.h
settings/notificationssettingspage.h settings/notificationssettingspage.h
settings/scrobblersettingspage.h settings/scrobblersettingspage.h
settings/lyricssettingspage.h
dialogs/about.h dialogs/about.h
dialogs/errordialog.h dialogs/errordialog.h
@ -492,6 +500,7 @@ set(UI
settings/appearancesettingspage.ui settings/appearancesettingspage.ui
settings/notificationssettingspage.ui settings/notificationssettingspage.ui
settings/scrobblersettingspage.ui settings/scrobblersettingspage.ui
settings/lyricssettingspage.ui
equalizer/equalizer.ui equalizer/equalizer.ui
equalizer/equalizerslider.ui equalizer/equalizerslider.ui

View File

@ -60,8 +60,11 @@
#include "lyrics/lyricsproviders.h" #include "lyrics/lyricsproviders.h"
#include "lyrics/auddlyricsprovider.h" #include "lyrics/auddlyricsprovider.h"
#include "lyrics/geniuslyricsprovider.h"
#include "lyrics/ovhlyricsprovider.h" #include "lyrics/ovhlyricsprovider.h"
#include "lyrics/lololyricsprovider.h" #include "lyrics/lololyricsprovider.h"
#include "lyrics/musixmatchlyricsprovider.h"
#include "lyrics/chartlyricsprovider.h"
#include "scrobbler/audioscrobbler.h" #include "scrobbler/audioscrobbler.h"
@ -132,8 +135,12 @@ class ApplicationImpl {
lyrics_providers_([=]() { lyrics_providers_([=]() {
LyricsProviders *lyrics_providers = new LyricsProviders(app); LyricsProviders *lyrics_providers = new LyricsProviders(app);
lyrics_providers->AddProvider(new AuddLyricsProvider(app)); lyrics_providers->AddProvider(new AuddLyricsProvider(app));
lyrics_providers->AddProvider(new GeniusLyricsProvider(app));
lyrics_providers->AddProvider(new OVHLyricsProvider(app)); lyrics_providers->AddProvider(new OVHLyricsProvider(app));
lyrics_providers->AddProvider(new LoloLyricsProvider(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; return lyrics_providers;
}), }),
internet_services_([=]() { internet_services_([=]() {

View File

@ -134,6 +134,7 @@
#include "covermanager/albumcoverchoicecontroller.h" #include "covermanager/albumcoverchoicecontroller.h"
#include "covermanager/albumcoverloaderresult.h" #include "covermanager/albumcoverloaderresult.h"
#include "covermanager/currentalbumcoverloader.h" #include "covermanager/currentalbumcoverloader.h"
#include "lyrics/lyricsproviders.h"
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
# include "device/devicemanager.h" # include "device/devicemanager.h"
# include "device/devicestatefiltermodel.h" # include "device/devicestatefiltermodel.h"
@ -950,6 +951,7 @@ void MainWindow::ReloadAllSettings() {
album_cover_choice_controller_->ReloadSettings(); album_cover_choice_controller_->ReloadSettings();
if (cover_manager_.get()) cover_manager_->ReloadSettings(); if (cover_manager_.get()) cover_manager_->ReloadSettings();
context_view_->ReloadSettings(); context_view_->ReloadSettings();
app_->lyrics_providers()->ReloadSettings();
#ifdef HAVE_SUBSONIC #ifdef HAVE_SUBSONIC
subsonic_view_->ReloadSettings(); subsonic_view_->ReloadSettings();
#endif #endif

View File

@ -49,9 +49,10 @@
#include <QSslSocket> #include <QSslSocket>
#include <QDateTime> #include <QDateTime>
LocalRedirectServer::LocalRedirectServer(const bool https, QObject *parent) LocalRedirectServer::LocalRedirectServer(QObject *parent)
: QTcpServer(parent), : QTcpServer(parent),
https_(https), https_(false),
port_(0),
socket_(nullptr) socket_(nullptr)
{} {}
@ -232,7 +233,7 @@ bool LocalRedirectServer::Listen() {
if (https_) { if (https_) {
if (!GenerateCertificate()) return false; if (!GenerateCertificate()) return false;
} }
if (!listen(QHostAddress::LocalHost)) { if (!listen(QHostAddress::LocalHost, port_)) {
error_ = errorString(); error_ = errorString();
return false; return false;
} }

View File

@ -38,9 +38,11 @@ class LocalRedirectServer : public QTcpServer {
Q_OBJECT Q_OBJECT
public: public:
explicit LocalRedirectServer(const bool https, QObject* parent = nullptr); explicit LocalRedirectServer(QObject *parent = nullptr);
~LocalRedirectServer(); ~LocalRedirectServer();
void set_https(const bool https) { https_ = https; }
void set_port(const int port) { port_ = port; }
bool Listen(); bool Listen();
const QUrl &url() const { return url_; } const QUrl &url() const { return url_; }
const QUrl &request_url() const { return request_url_; } const QUrl &request_url() const { return request_url_; }
@ -65,6 +67,7 @@ class LocalRedirectServer : public QTcpServer {
private: private:
bool https_; bool https_;
int port_;
QUrl url_; QUrl url_;
QUrl request_url_; QUrl request_url_;
QSslCertificate ssl_certificate_; QSslCertificate ssl_certificate_;

View File

@ -46,7 +46,7 @@ const char *AuddLyricsProvider::kUrlSearch = "https://api.audd.io/findLyrics/";
const char *AuddLyricsProvider::kAPITokenB64 = "ZjA0NjQ4YjgyNDM3ZTc1MjY3YjJlZDI5ZDBlMzQxZjk="; const char *AuddLyricsProvider::kAPITokenB64 = "ZjA0NjQ4YjgyNDM3ZTc1MjY3YjJlZDI5ZDBlMzQxZjk=";
const int AuddLyricsProvider::kMaxLength = 6000; 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) { 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(); reply->deleteLater();
QJsonArray json_result = ExtractResult(reply, id, artist, title); QJsonArray json_result = ExtractResult(reply, artist, title);
if (json_result.isEmpty()) { if (json_result.isEmpty()) {
emit SearchFinished(id, LyricsSearchResults());
return; return;
} }
@ -110,10 +111,7 @@ void AuddLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
result.lyrics = json_obj["lyrics"].toString(); result.lyrics = json_obj["lyrics"].toString();
if (result.lyrics.length() > kMaxLength) continue; if (result.lyrics.length() > kMaxLength) continue;
if (result.lyrics == "error") 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(); //qLog(Debug) << "AudDLyrics:" << result.artist << result.title << result.lyrics.length();
results << result; 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.isEmpty()) return QJsonArray();
if (!json_obj.contains("status")) { if (!json_obj.contains("status")) {
Error(id, "Json reply is missing status.", json_obj); Error("Json reply is missing status.", json_obj);
return QJsonArray(); return QJsonArray();
} }
if (json_obj["status"].toString() == "error") { if (json_obj["status"].toString() == "error") {
if (!json_obj.contains("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(); return QJsonArray();
} }
QJsonObject json_error = json_obj["error"].toObject(); QJsonObject json_error = json_obj["error"].toObject();
if (!json_error.contains("error_code") || !json_error.contains("error_message")) { 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(); return QJsonArray();
} }
QString error_code(json_error["error_code"].toString()); QString error_code(json_error["error_code"].toString());
QString error_message(json_error["error_message"].toString()); QString error_message(json_error["error_message"].toString());
Error(id, error_message); Error(error_message);
return QJsonArray(); return QJsonArray();
} }
if (!json_obj.contains("result")) { if (!json_obj.contains("result")) {
Error(id, "Json reply is missing result.", json_obj); Error("Json reply is missing result.", json_obj);
return QJsonArray(); return QJsonArray();
} }
QJsonArray json_result = json_obj["result"].toArray(); QJsonArray json_result = json_obj["result"].toArray();
if (json_result.isEmpty()) { 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(); 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; qLog(Error) << "AudDLyrics:" << error;
if (debug.isValid()) qLog(Debug) << debug; if (debug.isValid()) qLog(Debug) << debug;
emit SearchFinished(id, LyricsSearchResults());
} }

View File

@ -51,9 +51,9 @@ class AuddLyricsProvider : public JsonLyricsProvider {
static const char *kAPITokenB64; static const char *kAPITokenB64;
static const int kMaxLength; static const int kMaxLength;
QNetworkAccessManager *network_; 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);
}; };

View File

@ -0,0 +1,129 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QList>
#include <QPair>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QXmlStreamReader>
#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 &param : 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;
}

View File

@ -0,0 +1,56 @@
/*
* Strawberry Music Player
* Copyright 2018, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef CHARTLYRICSPROVIDER_H
#define CHARTLYRICSPROVIDER_H
#include "config.h"
#include <QObject>
#include <QVariant>
#include <QString>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#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

View File

@ -0,0 +1,536 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <memory.h>
#include <QObject>
#include <QPair>
#include <QList>
#include <QByteArray>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QTextCodec>
#include <QDesktopServices>
#include <QCryptographicHash>
#include <QSettings>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QJsonParseError>
#include <QMessageBox>
#include <QtDebug>
#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 &param : 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(":<br /><a href=\"%1\">%1</a>").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 &param : 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<QSslError>)), this, SLOT(HandleLoginSSLErrors(QList<QSslError>)));
connect(reply, &QNetworkReply::finished, [=] { AccessTokenRequestFinished(reply); });
}
else {
AuthError(tr("Redirect from Genius is missing query items code or state."));
return;
}
}
void GeniusLyricsProvider::HandleLoginSSLErrors(QList<QSslError> 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<GeniusLyricsSearchContext> search = std::make_shared<GeniusLyricsSearchContext>();
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 &param : 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<GeniusLyricsSearchContext> 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<GeniusLyricsSearchContext> 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 = "<div class=\"lyrics\">";
QString tag_end = "</div>";
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<GeniusLyricsSearchContext> 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);
}
}

View File

@ -0,0 +1,108 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef GENIUSLYRICSPROVIDER_H
#define GENIUSLYRICSPROVIDER_H
#include "config.h"
#include <memory.h>
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSslError>
#include <QJsonArray>
#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<QUrl, GeniusLyricsLyricContext> 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<GeniusLyricsSearchContext> search, const GeniusLyricsLyricContext lyric = GeniusLyricsLyricContext());
private slots:
void HandleLoginSSLErrors(QList<QSslError> 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<int, std::shared_ptr<GeniusLyricsSearchContext>> requests_search_;
};
#endif // GENIUSLYRICSPROVIDER_H

View File

@ -30,26 +30,27 @@
#include "lyricsprovider.h" #include "lyricsprovider.h"
#include "jsonlyricsprovider.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) { 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) { if (reply->error() < 200) {
Error(id, failure_reason); return QByteArray();
return QJsonObject();
} }
} }
else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { 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 (data.isEmpty()) {
if (failure_reason.isEmpty()) failure_reason = "Empty reply received from server.";
Error(id, failure_reason);
return QJsonObject(); return QJsonObject();
} }
@ -57,26 +58,32 @@ QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply, const quint
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) { 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(); return QJsonObject();
} }
if (json_doc.isNull() || json_doc.isEmpty()) { if (json_doc.isEmpty()) {
Error(id, "Received empty Json document."); Error("Received empty Json document.", data);
return QJsonObject(); return QJsonObject();
} }
if (!json_doc.isObject()) { if (!json_doc.isObject()) {
Error(id, "Json document is not an object."); Error("Json document is not an object.", json_doc);
return QJsonObject(); return QJsonObject();
} }
QJsonObject json_obj = json_doc.object(); QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) { if (json_obj.isEmpty()) {
Error(id, "Received empty Json object."); Error("Received empty Json object.", json_doc);
return QJsonObject(); return QJsonObject();
} }
return json_obj; return json_obj;
} }
QJsonObject JsonLyricsProvider::ExtractJsonObj(QNetworkReply *reply) {
return ExtractJsonObj(ExtractData(reply));
}

View File

@ -36,11 +36,14 @@ class JsonLyricsProvider : public LyricsProvider {
Q_OBJECT Q_OBJECT
public: public:
explicit JsonLyricsProvider(const QString &name, QObject *parent = nullptr); explicit JsonLyricsProvider(const QString &name, const bool enabled = true, const bool authentication_required = false, QObject *parent = nullptr);
QJsonObject ExtractJsonObj(QNetworkReply *reply, const quint64 id);
QByteArray ExtractData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
QJsonObject ExtractJsonObj(QNetworkReply *reply);
private: 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;
}; };

View File

@ -41,7 +41,7 @@
const char *LoloLyricsProvider::kUrlSearch = "http://api.lololyrics.com/0.5/getLyric"; 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) { 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) { if (reply->error() != QNetworkReply::NoError) {
failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
if (reply->error() < 200) { if (reply->error() < 200) {
Error(id, failure_reason); Error(failure_reason);
emit SearchFinished(id, LyricsSearchResults());
return; return;
} }
} }
@ -119,12 +120,6 @@ void LoloLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 i
else if (type == QXmlStreamReader::EndElement) { else if (type == QXmlStreamReader::EndElement) {
if (name == "result") { if (name == "result") {
if (!result.lyrics.isEmpty()) { 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; results << result;
} }
result = LyricsSearchResult(); 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; qLog(Error) << "LoloLyrics:" << error;
if (debug.isValid()) qLog(Debug) << debug; if (debug.isValid()) qLog(Debug) << debug;
emit SearchFinished(id, LyricsSearchResults());
} }

View File

@ -47,7 +47,7 @@ class LoloLyricsProvider : public LyricsProvider {
private: private:
static const char *kUrlSearch; static const char *kUrlSearch;
QNetworkAccessManager *network_; QNetworkAccessManager *network_;
void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()); void Error(const QString &error, const QVariant &debug = QVariant());
}; };

View File

@ -29,7 +29,6 @@
#include "lyricsfetchersearch.h" #include "lyricsfetchersearch.h"
const int LyricsFetcher::kMaxConcurrentRequests = 5; const int LyricsFetcher::kMaxConcurrentRequests = 5;
const int LyricsFetcher::kGoodLyricsLength = 60;
LyricsFetcher::LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent) LyricsFetcher::LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent)
: QObject(parent), : QObject(parent),
@ -104,8 +103,9 @@ void LyricsFetcher::StartRequests() {
void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsSearchResults &results) { void LyricsFetcher::SingleSearchFinished(const quint64 request_id, const LyricsSearchResults &results) {
if (!active_requests_.contains(request_id)) return;
LyricsFetcherSearch *search = active_requests_.take(request_id); LyricsFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
search->deleteLater(); search->deleteLater();
emit SearchFinished(request_id, results); 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) { 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); LyricsFetcherSearch *search = active_requests_.take(request_id);
if (!search) return;
search->deleteLater(); search->deleteLater();
emit LyricsFetched(request_id, provider, lyrics); emit LyricsFetched(request_id, provider, lyrics);

View File

@ -53,9 +53,9 @@ struct LyricsSearchResult {
QString lyrics; QString lyrics;
float score; float score;
}; };
Q_DECLARE_METATYPE(LyricsSearchResult)
typedef QList<LyricsSearchResult> LyricsSearchResults; typedef QList<LyricsSearchResult> LyricsSearchResults;
Q_DECLARE_METATYPE(LyricsSearchResult)
Q_DECLARE_METATYPE(QList<LyricsSearchResult>) Q_DECLARE_METATYPE(QList<LyricsSearchResult>)
class LyricsFetcher : public QObject { class LyricsFetcher : public QObject {
@ -65,13 +65,13 @@ class LyricsFetcher : public QObject {
explicit LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent = nullptr); explicit LyricsFetcher(LyricsProviders *lyrics_providers, QObject *parent = nullptr);
virtual ~LyricsFetcher() {} virtual ~LyricsFetcher() {}
static const int kMaxConcurrentRequests;
static const int kGoodLyricsLength;
quint64 Search(const QString &artist, const QString &album, const QString &title); quint64 Search(const QString &artist, const QString &album, const QString &title);
void Clear(); void Clear();
signals: private:
void AddRequest(const LyricsSearchRequest &req);
signals:
void LyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics); void LyricsFetched(const quint64 request_id, const QString &provider, const QString &lyrics);
void SearchFinished(const quint64 request_id, const LyricsSearchResults &results); void SearchFinished(const quint64 request_id, const LyricsSearchResults &results);
@ -81,7 +81,7 @@ signals:
void StartRequests(); void StartRequests();
private: private:
void AddRequest(const LyricsSearchRequest &req); static const int kMaxConcurrentRequests;
LyricsProviders *lyrics_providers_; LyricsProviders *lyrics_providers_;
quint64 next_id_; quint64 next_id_;

View File

@ -19,6 +19,8 @@
#include "config.h" #include "config.h"
#include <algorithm>
#include <QObject> #include <QObject>
#include <QTimer> #include <QTimer>
#include <QList> #include <QList>
@ -30,10 +32,11 @@
#include "lyricsprovider.h" #include "lyricsprovider.h"
#include "lyricsproviders.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( LyricsFetcherSearch::LyricsFetcherSearch(const LyricsSearchRequest &request, QObject *parent)
const LyricsSearchRequest &request, QObject *parent)
: QObject(parent), : QObject(parent),
request_(request), request_(request),
cancel_requested_(false) { cancel_requested_(false) {
@ -53,7 +56,11 @@ void LyricsFetcherSearch::TerminateSearch() {
void LyricsFetcherSearch::Start(LyricsProviders *lyrics_providers) { void LyricsFetcherSearch::Start(LyricsProviders *lyrics_providers) {
for (LyricsProvider *provider : lyrics_providers->List()) { QList<LyricsProvider*> 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))); connect(provider, SIGNAL(SearchFinished(quint64, LyricsSearchResults)), SLOT(ProviderSearchFinished(quint64, LyricsSearchResults)));
const int id = lyrics_providers->NextId(); const int id = lyrics_providers->NextId();
const bool success = provider->StartSearch(request_.artist, request_.album, request_.title, id); 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); LyricsProvider *provider = pending_requests_.take(id);
LyricsSearchResults results_copy(results); 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].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); results_.append(results_copy);
std::stable_sort(results_.begin(), results_.end(), LyricsSearchResultCompareScore);
if (!pending_requests_.isEmpty()) { 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; return;
} }
@ -89,14 +120,10 @@ void LyricsFetcherSearch::AllProvidersFinished() {
if (cancel_requested_) return; if (cancel_requested_) return;
if (!results_.isEmpty()) { if (!results_.isEmpty()) {
LyricsSearchResult result_use; qLog(Debug) << "Using lyrics from" << results_.last().provider << "for" << request_.artist << request_.title << "with score" << results_.last().score;
result_use.score = 0.0; emit LyricsFetched(request_.id, results_.last().provider, results_.last().lyrics);
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);
} }
emit SearchFinished(request_.id, results_); 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;
}

View File

@ -51,11 +51,13 @@ class LyricsFetcherSearch : public QObject {
private: private:
void AllProvidersFinished(); void AllProvidersFinished();
static bool ProviderCompareOrder(LyricsProvider *a, LyricsProvider *b);
void SendBestImage(); static bool LyricsSearchResultCompareScore(const LyricsSearchResult &a, const LyricsSearchResult &b);
private: private:
static const int kSearchTimeoutMs; static const int kSearchTimeoutMs;
static const int kGoodLyricsLength;
static const float kHighScore;
LyricsSearchRequest request_; LyricsSearchRequest request_;
LyricsSearchResults results_; LyricsSearchResults results_;

View File

@ -24,5 +24,5 @@
#include "lyricsprovider.h" #include "lyricsprovider.h"
LyricsProvider::LyricsProvider(const QString &name, QObject *parent) LyricsProvider::LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent)
: QObject(parent), name_(name) {} : QObject(parent), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required) {}

View File

@ -34,22 +34,36 @@ class LyricsProvider : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit LyricsProvider(const QString &name, QObject *parent); explicit LyricsProvider(const QString &name, const bool enabled, const bool authentication_required, QObject *parent);
typedef QPair<QString, QString> Param; typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList; typedef QList<Param> ParamList;
QString name() const { return name_; } 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 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 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: signals:
void AuthenticationComplete(bool, QStringList = QStringList());
void AuthenticationSuccess();
void AuthenticationFailure(QStringList);
void SearchFinished(const quint64 id, const LyricsSearchResults &results); void SearchFinished(const quint64 id, const LyricsSearchResults &results);
private: private:
QString name_; QString name_;
bool enabled_;
int order_;
bool authentication_required_;
}; };
#endif // LYRICSPROVIDER_H #endif // LYRICSPROVIDER_H

View File

@ -21,13 +21,20 @@
#include <QObject> #include <QObject>
#include <QMutex> #include <QMutex>
#include <QList>
#include <QVariant>
#include <QVariantList>
#include <QString> #include <QString>
#include <QStringList>
#include <QSettings>
#include <QtDebug> #include <QtDebug>
#include "core/logging.h" #include "core/logging.h"
#include "lyricsprovider.h" #include "lyricsprovider.h"
#include "lyricsproviders.h" #include "lyricsproviders.h"
#include "settings/lyricssettingspage.h"
LyricsProviders::LyricsProviders(QObject *parent) : QObject(parent) {} LyricsProviders::LyricsProviders(QObject *parent) : QObject(parent) {}
LyricsProviders::~LyricsProviders() { 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<LyricsProvider*> 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) { void LyricsProviders::AddProvider(LyricsProvider *provider) {
{ {

View File

@ -39,6 +39,9 @@ class LyricsProviders : public QObject {
explicit LyricsProviders(QObject *parent = nullptr); explicit LyricsProviders(QObject *parent = nullptr);
~LyricsProviders(); ~LyricsProviders();
void ReloadSettings();
LyricsProvider *ProviderByName(const QString &name) const;
void AddProvider(LyricsProvider *provider); void AddProvider(LyricsProvider *provider);
void RemoveProvider(LyricsProvider *provider); void RemoveProvider(LyricsProvider *provider);
QList<LyricsProvider*> List() const { return lyrics_providers_.keys(); } QList<LyricsProvider*> List() const { return lyrics_providers_.keys(); }
@ -51,7 +54,8 @@ class LyricsProviders : public QObject {
private: private:
Q_DISABLE_COPY(LyricsProviders) Q_DISABLE_COPY(LyricsProviders)
QMap<LyricsProvider *, QString> lyrics_providers_; QMap<LyricsProvider*, QString> lyrics_providers_;
QList<LyricsProvider*> ordered_providers_;
QMutex mutex_; QMutex mutex_;
QAtomicInt next_id_; QAtomicInt next_id_;

View File

@ -0,0 +1,214 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <memory.h>
#include <QObject>
#include <QByteArray>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTextCodec>
#include <QJsonObject>
#include <QtDebug>
#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 = ";</script>";
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;
}

View File

@ -0,0 +1,64 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef MUSIXMATCHLYRICSPROVIDER_H
#define MUSIXMATCHLYRICSPROVIDER_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QJsonArray>
#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

View File

@ -38,7 +38,7 @@
const char *OVHLyricsProvider::kUrlSearch = "https://api.lyrics.ovh/v1/"; 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) { 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(); reply->deleteLater();
QJsonObject json_obj = ExtractJsonObj(reply, id); QJsonObject json_obj = ExtractJsonObj(reply);
if (json_obj.isEmpty()) { if (json_obj.isEmpty()) {
emit SearchFinished(id, LyricsSearchResults()); emit SearchFinished(id, LyricsSearchResults());
return; return;
} }
if (json_obj.contains("error")) { if (json_obj.contains("error")) {
Error(id, json_obj["error"].toString()); Error(json_obj["error"].toString());
qLog(Debug) << "OVHLyrics: No lyrics for" << artist << title; qLog(Debug) << "OVHLyrics: No lyrics for" << artist << title;
emit SearchFinished(id, LyricsSearchResults()); emit SearchFinished(id, LyricsSearchResults());
return; return;
@ -82,19 +82,17 @@ void OVHLyricsProvider::HandleSearchReply(QNetworkReply *reply, const quint64 id
LyricsSearchResult result; LyricsSearchResult result;
result.lyrics = json_obj["lyrics"].toString(); result.lyrics = json_obj["lyrics"].toString();
if (result.lyrics.length() > LyricsFetcher::kGoodLyricsLength)
result.score += 1.0;
qLog(Debug) << "OVHLyrics: Got lyrics for" << artist << title; qLog(Debug) << "OVHLyrics: Got lyrics for" << artist << title;
emit SearchFinished(id, LyricsSearchResults() << result); 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; qLog(Error) << "OVHLyrics:" << error;
if (debug.isValid()) qLog(Debug) << debug; if (debug.isValid()) qLog(Debug) << debug;
emit SearchFinished(id, LyricsSearchResults());
} }

View File

@ -47,7 +47,7 @@ class OVHLyricsProvider : public JsonLyricsProvider {
private: private:
static const char *kUrlSearch; static const char *kUrlSearch;
QNetworkAccessManager *network_; QNetworkAccessManager *network_;
void Error(const quint64 id, const QString &error, const QVariant &debug = QVariant()); void Error(const QString &error, const QVariant &debug = QVariant());
}; };

View File

@ -122,7 +122,8 @@ void ListenBrainzScrobbler::Logout() {
void ListenBrainzScrobbler::Authenticate(const bool https) { void ListenBrainzScrobbler::Authenticate(const bool https) {
if (!server_) { if (!server_) {
server_ = new LocalRedirectServer(https, this); server_ = new LocalRedirectServer(this);
server_->set_https(https);
if (!server_->Listen()) { if (!server_->Listen()) {
AuthError(server_->error()); AuthError(server_->error());
delete server_; delete server_;

View File

@ -126,7 +126,8 @@ void ScrobblingAPI20::Logout() {
void ScrobblingAPI20::Authenticate(const bool https) { void ScrobblingAPI20::Authenticate(const bool https) {
if (!server_) { if (!server_) {
server_ = new LocalRedirectServer(https, this); server_ = new LocalRedirectServer(this);
server_->set_https(https);
if (!server_->Listen()) { if (!server_->Listen()) {
AuthError(server_->error()); AuthError(server_->error());
delete server_; delete server_;

View File

@ -0,0 +1,251 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QPalette>
#include <QSettings>
#include <QGroupBox>
#include <QPushButton>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMessageBox>
#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<LyricsProvider*> 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<LyricsProvider*>(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<LyricsProvider*>(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();
}

View File

@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef LYRICSSETTINGSPAGE_H
#define LYRICSSETTINGSPAGE_H
#include "config.h"
#include <QObject>
#include <QString>
#include <QStringList>
#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

View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LyricsSettingsPage</class>
<widget class="QWidget" name="LyricsSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>460</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Lyrics</string>
</property>
<layout class="QVBoxLayout" name="layout_tidalsettingspage">
<item>
<widget class="QGroupBox" name="groupbox_providers">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Lyrics providers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_15">
<item>
<widget class="QLabel" name="label_enabled">
<property name="text">
<string>Choose the providers you want to use when searching for lyrics.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_providers">
<item>
<widget class="QListWidget" name="providers"/>
</item>
<item>
<layout class="QVBoxLayout" name="layout_providers_updown">
<item>
<widget class="QPushButton" name="providers_up">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Move up</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="providers_down">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Move down</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_providers_updown">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="authentication">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Authentication</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_auth_info">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<layout class="QHBoxLayout" name="layout_button_authenticate">
<item>
<widget class="QPushButton" name="button_authenticate">
<property name="text">
<string>Authenticate</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_button_authenticate">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_bottom">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>30</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../data/data.qrc"/>
<include location="../../data/icons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -56,17 +56,18 @@
#include "settingsdialog.h" #include "settingsdialog.h"
#include "settingspage.h" #include "settingspage.h"
#include "appearancesettingspage.h"
#include "backendsettingspage.h"
#include "behavioursettingspage.h" #include "behavioursettingspage.h"
#include "contextsettingspage.h"
#include "collectionsettingspage.h" #include "collectionsettingspage.h"
#include "notificationssettingspage.h" #include "backendsettingspage.h"
#include "playlistsettingspage.h" #include "playlistsettingspage.h"
#include "appearancesettingspage.h"
#include "contextsettingspage.h"
#include "scrobblersettingspage.h"
#include "notificationssettingspage.h"
#include "shortcutssettingspage.h" #include "shortcutssettingspage.h"
#include "transcodersettingspage.h" #include "transcodersettingspage.h"
#include "networkproxysettingspage.h" #include "networkproxysettingspage.h"
#include "scrobblersettingspage.h" #include "lyricssettingspage.h"
#ifdef HAVE_MOODBAR #ifdef HAVE_MOODBAR
# include "moodbarsettingspage.h" # include "moodbarsettingspage.h"
#endif #endif
@ -138,6 +139,7 @@ SettingsDialog::SettingsDialog(Application *app, QMainWindow *mainwindow, QWidge
QTreeWidgetItem *iface = AddCategory(tr("User interface")); QTreeWidgetItem *iface = AddCategory(tr("User interface"));
AddPage(Page_Appearance, new AppearanceSettingsPage(this), iface); AddPage(Page_Appearance, new AppearanceSettingsPage(this), iface);
AddPage(Page_Context, new ContextSettingsPage(this), iface); AddPage(Page_Context, new ContextSettingsPage(this), iface);
AddPage(Page_Lyrics, new LyricsSettingsPage(this), iface);
AddPage(Page_Notifications, new NotificationsSettingsPage(this), iface); AddPage(Page_Notifications, new NotificationsSettingsPage(this), iface);
#ifdef HAVE_GLOBALSHORTCUTS #ifdef HAVE_GLOBALSHORTCUTS

View File

@ -81,6 +81,7 @@ class SettingsDialog : public QDialog {
Page_GlobalShortcuts, Page_GlobalShortcuts,
Page_Appearance, Page_Appearance,
Page_Context, Page_Context,
Page_Lyrics,
Page_Notifications, Page_Notifications,
Page_Transcoding, Page_Transcoding,
Page_Proxy, Page_Proxy,