From 6dcdf5bf9265b668f04dd335533339b87c8d7489 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 14 Apr 2019 02:54:40 +0200 Subject: [PATCH] Add deezer cover provider --- README.md | 2 +- dist/debian/control | 2 +- dist/man/strawberry.1 | 2 +- dist/rpm/strawberry.spec.in | 2 +- src/CMakeLists.txt | 2 + src/core/application.cpp | 2 + src/covermanager/deezercoverprovider.cpp | 284 +++++++++++++++++++++++ src/covermanager/deezercoverprovider.h | 63 +++++ 8 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 src/covermanager/deezercoverprovider.cpp create mode 100644 src/covermanager/deezercoverprovider.h diff --git a/README.md b/README.md index 1166b87c..22a125b7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Strawberry is a music player and music collection organizer. It is a fork of Cle * 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 + * Album cover art from Last.fm, Musicbrainz, Discogs and Deezer * Song lyrics from AudD * Support for multiple backends * Audio analyzer diff --git a/dist/debian/control b/dist/debian/control index 7ba53c6e..f2907080 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -58,7 +58,7 @@ Description: Audio player and music collection organizer - 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 Lastfm, Musicbrainz and Discogs + - Album cover art from Lastfm, Musicbrainz, Discogs and Deezer - Song lyrics from AudD - Support for multiple backends - Audio analyzer diff --git a/dist/man/strawberry.1 b/dist/man/strawberry.1 index a91960a8..6a3dba16 100644 --- a/dist/man/strawberry.1 +++ b/dist/man/strawberry.1 @@ -25,7 +25,7 @@ Features: .br - Fetch tags from MusicBrainz .br -- Album cover art from Lastfm, Musicbrainz and Discogs +- Album cover art from Lastfm, Musicbrainz, Discogs and Deezer .br - Song lyrics from AudD .br diff --git a/dist/rpm/strawberry.spec.in b/dist/rpm/strawberry.spec.in index 810796d9..00783867 100644 --- a/dist/rpm/strawberry.spec.in +++ b/dist/rpm/strawberry.spec.in @@ -96,7 +96,7 @@ 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 and Discogs + - Album cover art from Last.fm, Musicbrainz, Discogs and Deezer - Song lyrics from AudD - Support for multiple backends - Audio analyzer diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 88445f5b..eb04938f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,6 +198,7 @@ set(SOURCES covermanager/lastfmcoverprovider.cpp covermanager/musicbrainzcoverprovider.cpp covermanager/discogscoverprovider.cpp + covermanager/deezercoverprovider.cpp lyrics/lyricsproviders.cpp lyrics/lyricsprovider.cpp @@ -373,6 +374,7 @@ set(HEADERS covermanager/lastfmcoverprovider.h covermanager/musicbrainzcoverprovider.h covermanager/discogscoverprovider.h + covermanager/deezercoverprovider.h lyrics/lyricsproviders.h lyrics/lyricsprovider.h diff --git a/src/core/application.cpp b/src/core/application.cpp index 393d3e96..9fd960ab 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -54,6 +54,7 @@ #include "covermanager/lastfmcoverprovider.h" #include "covermanager/discogscoverprovider.h" #include "covermanager/musicbrainzcoverprovider.h" +#include "covermanager/deezercoverprovider.h" #include "lyrics/lyricsproviders.h" #include "lyrics/lyricsprovider.h" @@ -105,6 +106,7 @@ class ApplicationImpl { cover_providers->AddProvider(new LastFmCoverProvider(app)); cover_providers->AddProvider(new DiscogsCoverProvider(app)); cover_providers->AddProvider(new MusicbrainzCoverProvider(app)); + cover_providers->AddProvider(new DeezerCoverProvider(app)); return cover_providers; }), album_cover_loader_([=]() { diff --git a/src/covermanager/deezercoverprovider.cpp b/src/covermanager/deezercoverprovider.cpp new file mode 100644 index 00000000..f375cd1f --- /dev/null +++ b/src/covermanager/deezercoverprovider.cpp @@ -0,0 +1,284 @@ +/* + * 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 +#include +#include +#include + +#include "core/closure.h" +#include "core/network.h" +#include "core/logging.h" +#include "albumcoverfetcher.h" +#include "coverprovider.h" +#include "deezercoverprovider.h" + +const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com"; +const int DeezerCoverProvider::kLimit = 10; + +DeezerCoverProvider::DeezerCoverProvider(QObject *parent): CoverProvider("Deezer", true, parent), network_(new NetworkAccessManager(this)) {} + +bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, int id) { + + typedef QPair Param; + typedef QList Parameters; + typedef QPair EncodedParam; + typedef QList EncodedParamList; + + Parameters params = Parameters() << Param("output", "json") + << Param("q", QString(album + " " + artist)) + << Param("limit", QString::number(kLimit)); + + QUrlQuery url_query; + for (const Param ¶m : params) { + EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); + url_query.addQueryItem(encoded_param.first, encoded_param.second); + } + + QUrl url(kApiUrl + QString("/search/album")); + url.setQuery(url_query); + QNetworkRequest req(url); + QNetworkReply *reply = network_->get(req); + + NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id); + + return true; + +} + +void DeezerCoverProvider::CancelSearch(int id) {} + +QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) { + + QByteArray data; + + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + data = reply->readAll(); + } + else { + if (reply->error() < 200) { + // This is a network error, there is nothing more to do. + QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(failure_reason); + } + else { + // See if there is Json data containing "error" - then use that instead. + data = reply->readAll(); + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + QString failure_reason; + if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { + QJsonObject json_obj = json_doc.object(); + if (json_obj.contains("error")) { + QJsonValue json_value_error = json_obj["error"]; + if (json_value_error.isObject()) { + QJsonObject json_error = json_value_error.toObject(); + int code = json_error["code"].toInt(); + QString message = json_error["message"].toString(); + QString type = json_error["type"].toString(); + failure_reason = QString("%1 (%2)").arg(message).arg(code); + } + } + } + if (failure_reason.isEmpty()) failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); + Error(failure_reason); + } + return QByteArray(); + } + + return data; + +} + +QJsonObject DeezerCoverProvider::ExtractJsonObj(QByteArray &data) { + + QJsonParseError error; + QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + Error("Reply from server missing Json data.", data); + return QJsonObject(); + } + + if (json_doc.isNull() || json_doc.isEmpty()) { + Error("Received empty Json document.", json_doc); + return QJsonObject(); + } + + if (!json_doc.isObject()) { + Error("Json document is not an object.", json_doc); + return QJsonObject(); + } + + QJsonObject json_obj = json_doc.object(); + if (json_obj.isEmpty()) { + Error("Received empty Json object.", json_doc); + return QJsonObject(); + } + + return json_obj; + +} + +QJsonValue DeezerCoverProvider::ExtractData(QByteArray &data) { + + QJsonObject json_obj = ExtractJsonObj(data); + if (json_obj.isEmpty()) return QJsonObject(); + + if (json_obj.contains("error")) { + QJsonValue json_value_error = json_obj["error"]; + if (!json_value_error.isObject()) { + Error("Error missing object", json_obj); + return QJsonValue(); + } + QJsonObject json_error = json_value_error.toObject(); + int code = json_error["code"].toInt(); + QString message = json_error["message"].toString(); + QString type = json_error["type"].toString(); + Error(QString("%1 (%2)").arg(message).arg(code)); + return QJsonValue(); + } + + if (!json_obj.contains("data") && !json_obj.contains("DATA")) { + Error("Json reply is missing data.", json_obj); + return QJsonValue(); + } + + QJsonValue json_data; + if (json_obj.contains("data")) json_data = json_obj["data"]; + else json_data = json_obj["DATA"]; + + return json_data; + +} + +void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, int id) { + + reply->deleteLater(); + + CoverSearchResults results; + + QByteArray data = GetReplyData(reply); + if (data.isEmpty()) { + emit SearchFinished(id, results); + return; + } + + QJsonValue json_value = ExtractData(data); + if (!json_value.isArray()) { + emit SearchFinished(id, results); + return; + } + + QJsonArray json_data = json_value.toArray(); + if (json_data.isEmpty()) { + emit SearchFinished(id, results); + return; + } + + for (const QJsonValue &value : json_data) { + + if (!value.isObject()) { + Error("Invalid Json reply, data is not an object.", value); + continue; + } + QJsonObject json_obj = value.toObject(); + + if (!json_obj.contains("id") || !json_obj.contains("type")) { + Error("Invalid Json reply, item is missing ID or type.", json_obj); + continue; + } + + QString type = json_obj["type"].toString(); + if (type != "album") { + Error("Invalid Json reply, incorrect type returned", json_obj); + continue; + } + + if (!json_obj.contains("artist")) { + Error("Invalid Json reply, item missing artist.", json_obj); + continue; + } + QJsonValue json_value_artist = json_obj["artist"]; + if (!json_value_artist.isObject()) { + Error("Invalid Json reply, item artist is not a object.", json_value_artist); + continue; + } + QJsonObject json_artist = json_value_artist.toObject(); + + if (!json_artist.contains("name")) { + Error("Invalid Json reply, artist data missing name.", json_artist); + continue; + } + QString artist = json_artist["name"].toString(); + + if (!json_obj.contains("title")) { + Error("Invalid Json reply, data missing title.", json_obj); + continue; + } + QString album = json_obj["title"].toString(); + + QString cover; + if (json_obj.contains("cover_xl")) { + cover = json_obj["cover_xl"].toString(); + } + else if (json_obj.contains("cover_big")) { + cover = json_obj["cover_big"].toString(); + } + else if (json_obj.contains("cover_medium")) { + cover = json_obj["cover_medium"].toString(); + } + else if (json_obj.contains("cover_small")) { + cover = json_obj["cover_small"].toString(); + } + else { + Error("Invalid Json reply, data missing cover.", json_obj); + continue; + } + QUrl url(cover); + + CoverSearchResult cover_result; + cover_result.description = artist + " " + album; + cover_result.image_url = url; + results << cover_result; + + } + emit SearchFinished(id, results); + +} + +void DeezerCoverProvider::Error(QString error, QVariant debug) { + qLog(Error) << "Deezer:" << error; + if (debug.isValid()) qLog(Debug) << debug; +} diff --git a/src/covermanager/deezercoverprovider.h b/src/covermanager/deezercoverprovider.h new file mode 100644 index 00000000..444471d2 --- /dev/null +++ b/src/covermanager/deezercoverprovider.h @@ -0,0 +1,63 @@ +/* + * 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 DEEZERCOVERPROVIDER_H +#define DEEZERCOVERPROVIDER_H + +#include "config.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "coverprovider.h" + +class DeezerCoverProvider : public CoverProvider { + Q_OBJECT + + public: + explicit DeezerCoverProvider(QObject *parent = nullptr); + bool StartSearch(const QString &artist, const QString &album, int id); + void CancelSearch(int id); + + private slots: + void HandleSearchReply(QNetworkReply *reply, int id); + + private: + static const char *kApiUrl; + static const int kLimit; + + QByteArray GetReplyData(QNetworkReply *reply); + QJsonObject ExtractJsonObj(QByteArray &data); + QJsonValue ExtractData(QByteArray &data); + void Error(QString error, QVariant debug = QVariant()); + + QNetworkAccessManager *network_; + +}; + +#endif // DEEZERCOVERPROVIDER_H