1 /*
2  * Strawberry Music Player
3  * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
4  *
5  * Strawberry is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Strawberry is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #include "config.h"
21 
22 #include <algorithm>
23 
24 #include <QtGlobal>
25 #include <QObject>
26 #include <QPair>
27 #include <QSet>
28 #include <QList>
29 #include <QMap>
30 #include <QVariant>
31 #include <QByteArray>
32 #include <QString>
33 #include <QUrl>
34 #include <QUrlQuery>
35 #include <QNetworkRequest>
36 #include <QNetworkReply>
37 #include <QJsonDocument>
38 #include <QJsonValue>
39 #include <QJsonObject>
40 #include <QJsonArray>
41 #include <QtDebug>
42 
43 #include "core/application.h"
44 #include "core/networkaccessmanager.h"
45 #include "core/logging.h"
46 #include "core/song.h"
47 #include "albumcoverfetcher.h"
48 #include "albumcoverfetchersearch.h"
49 #include "jsoncoverprovider.h"
50 #include "deezercoverprovider.h"
51 
52 const char *DeezerCoverProvider::kApiUrl = "https://api.deezer.com";
53 const int DeezerCoverProvider::kLimit = 10;
54 
DeezerCoverProvider(Application * app,NetworkAccessManager * network,QObject * parent)55 DeezerCoverProvider::DeezerCoverProvider(Application *app, NetworkAccessManager *network, QObject *parent)
56     : JsonCoverProvider("Deezer", true, false, 2.0, true, true, app, network, parent) {}
57 
~DeezerCoverProvider()58 DeezerCoverProvider::~DeezerCoverProvider() {
59 
60   while (!replies_.isEmpty()) {
61     QNetworkReply *reply = replies_.takeFirst();
62     QObject::disconnect(reply, nullptr, this, nullptr);
63     reply->abort();
64     reply->deleteLater();
65   }
66 
67 }
68 
StartSearch(const QString & artist,const QString & album,const QString & title,const int id)69 bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
70 
71   typedef QPair<QString, QString> Param;
72   typedef QList<Param> Params;
73 
74   if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
75 
76   QString resource;
77   QString query = artist;
78   if (album.isEmpty() && !title.isEmpty()) {
79     resource = "search/track";
80     if (!query.isEmpty()) query.append(" ");
81     query.append(title);
82   }
83   else {
84     resource = "search/album";
85     if (!album.isEmpty()) {
86       if (!query.isEmpty()) query.append(" ");
87       query.append(album);
88     }
89   }
90 
91   const Params params = Params() << Param("output", "json")
92                                  << Param("q", query)
93                                  << Param("limit", QString::number(kLimit));
94 
95   QUrlQuery url_query;
96   for (const Param &param : params) {
97     url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
98   }
99 
100   QUrl url(kApiUrl + QString("/") + resource);
101   url.setQuery(url_query);
102   QNetworkRequest req(url);
103 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
104   req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
105 #else
106   req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
107 #endif
108   QNetworkReply *reply = network_->get(req);
109   replies_ << reply;
110   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
111 
112   return true;
113 
114 }
115 
CancelSearch(const int id)116 void DeezerCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
117 
GetReplyData(QNetworkReply * reply)118 QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
119 
120   QByteArray data;
121 
122   if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
123     data = reply->readAll();
124   }
125   else {
126     if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
127       // This is a network error, there is nothing more to do.
128       QString error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
129       Error(error);
130     }
131     else {
132       // See if there is Json data containing "error" object - then use that instead.
133       data = reply->readAll();
134       QJsonParseError json_error;
135       QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
136       QString error;
137       if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
138         QJsonObject json_obj = json_doc.object();
139         if (json_obj.contains("error")) {
140           QJsonValue value_error = json_obj["error"];
141           if (value_error.isObject()) {
142             QJsonObject obj_error = value_error.toObject();
143             int code = obj_error["code"].toInt();
144             QString message = obj_error["message"].toString();
145             error = QString("%1 (%2)").arg(message).arg(code);
146           }
147         }
148       }
149       if (error.isEmpty()) {
150         if (reply->error() != QNetworkReply::NoError) {
151           error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
152         }
153         else {
154           error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
155         }
156       }
157       Error(error);
158     }
159     return QByteArray();
160   }
161 
162   return data;
163 
164 }
165 
ExtractData(const QByteArray & data)166 QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
167 
168   QJsonObject json_obj = ExtractJsonObj(data);
169   if (json_obj.isEmpty()) return QJsonObject();
170 
171   if (json_obj.contains("error")) {
172     QJsonValue value_error = json_obj["error"];
173     if (!value_error.isObject()) {
174       Error("Error missing object", json_obj);
175       return QJsonValue();
176     }
177     QJsonObject obj_error = value_error.toObject();
178     const int code = obj_error["code"].toInt();
179     QString message = obj_error["message"].toString();
180     Error(QString("%1 (%2)").arg(message).arg(code));
181     return QJsonValue();
182   }
183 
184   if (!json_obj.contains("data") && !json_obj.contains("DATA")) {
185     Error("Json reply object is missing data.", json_obj);
186     return QJsonValue();
187   }
188 
189   QJsonValue value_data;
190   if (json_obj.contains("data")) value_data = json_obj["data"];
191   else value_data = json_obj["DATA"];
192 
193   return value_data;
194 
195 }
196 
HandleSearchReply(QNetworkReply * reply,const int id)197 void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
198 
199   if (!replies_.contains(reply)) return;
200   replies_.removeAll(reply);
201   QObject::disconnect(reply, nullptr, this, nullptr);
202   reply->deleteLater();
203 
204   QByteArray data = GetReplyData(reply);
205   if (data.isEmpty()) {
206     emit SearchFinished(id, CoverProviderSearchResults());
207     return;
208   }
209 
210   QJsonValue value_data = ExtractData(data);
211   if (!value_data.isArray()) {
212     emit SearchFinished(id, CoverProviderSearchResults());
213     return;
214   }
215 
216   QJsonArray array_data = value_data.toArray();
217   if (array_data.isEmpty()) {
218     emit SearchFinished(id, CoverProviderSearchResults());
219     return;
220   }
221 
222   QMap<QUrl, CoverProviderSearchResult> results;
223   int i = 0;
224   for (const QJsonValueRef json_value : array_data) {
225 
226     if (!json_value.isObject()) {
227       Error("Invalid Json reply, data array value is not a object.");
228       continue;
229     }
230     QJsonObject json_obj = json_value.toObject();
231     QJsonObject obj_album;
232     if (json_obj.contains("album") && json_obj["album"].isObject()) {  // Song search, so extract the album.
233       obj_album = json_obj["album"].toObject();
234     }
235     else {
236       obj_album = json_obj;
237     }
238 
239     if (!json_obj.contains("id") || !obj_album.contains("id")) {
240       Error("Invalid Json reply, data array value object is missing ID.", json_obj);
241       continue;
242     }
243 
244     if (!obj_album.contains("type")) {
245       Error("Invalid Json reply, data array value album object is missing type.", obj_album);
246       continue;
247     }
248     QString type = obj_album["type"].toString();
249     if (type != "album") {
250       Error("Invalid Json reply, data array value album object has incorrect type returned", obj_album);
251       continue;
252     }
253 
254     if (!json_obj.contains("artist")) {
255       Error("Invalid Json reply, data array value object is missing artist.", json_obj);
256       continue;
257     }
258     QJsonValue value_artist = json_obj["artist"];
259     if (!value_artist.isObject()) {
260       Error("Invalid Json reply, data array value artist is not a object.", value_artist);
261       continue;
262     }
263     QJsonObject obj_artist = value_artist.toObject();
264 
265     if (!obj_artist.contains("name")) {
266       Error("Invalid Json reply, data array value artist object is missing name.", obj_artist);
267       continue;
268     }
269     QString artist = obj_artist["name"].toString();
270 
271     if (!obj_album.contains("title")) {
272       Error("Invalid Json reply, data array value album object is missing title.", obj_album);
273       continue;
274     }
275     QString album = obj_album["title"].toString();
276 
277     album = album.remove(Song::kAlbumRemoveDisc);
278     album = album.remove(Song::kAlbumRemoveMisc);
279 
280     CoverProviderSearchResult cover_result;
281     cover_result.artist = artist;
282     cover_result.album = album;
283 
284     bool have_cover = false;
285     QList<QPair<QString, QSize>> cover_sizes = QList<QPair<QString, QSize>>() << qMakePair(QString("cover_xl"), QSize(1000, 1000))
286                                                                               << qMakePair(QString("cover_big"), QSize(500, 500));
287     for (const QPair<QString, QSize> &cover_size : cover_sizes) {
288       if (!obj_album.contains(cover_size.first)) continue;
289       QString cover = obj_album[cover_size.first].toString();
290       if (!have_cover) {
291         have_cover = true;
292         ++i;
293       }
294       QUrl url(cover);
295       if (!results.contains(url)) {
296         cover_result.image_url = url;
297         cover_result.image_size = cover_size.second;
298         cover_result.number = i;
299         results.insert(url, cover_result);
300       }
301     }
302 
303     if (!have_cover) {
304       Error("Invalid Json reply, data array value album object is missing cover.", obj_album);
305     }
306 
307   }
308 
309   if (results.isEmpty()) {
310     emit SearchFinished(id, CoverProviderSearchResults());
311   }
312   else {
313     CoverProviderSearchResults cover_results = results.values();
314     std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber);
315     emit SearchFinished(id, cover_results);
316   }
317 
318 }
319 
Error(const QString & error,const QVariant & debug)320 void DeezerCoverProvider::Error(const QString &error, const QVariant &debug) {
321   qLog(Error) << "Deezer:" << error;
322   if (debug.isValid()) qLog(Debug) << debug;
323 }
324