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 <QApplication>
26 #include <QDesktopServices>
27 #include <QLocale>
28 #include <QClipboard>
29 #include <QPair>
30 #include <QVariant>
31 #include <QByteArray>
32 #include <QString>
33 #include <QUrl>
34 #include <QUrlQuery>
35 #include <QDateTime>
36 #include <QTimer>
37 #include <QCryptographicHash>
38 #include <QMessageBox>
39 #include <QSettings>
40 #include <QNetworkRequest>
41 #include <QNetworkReply>
42 #include <QJsonDocument>
43 #include <QJsonObject>
44 #include <QJsonArray>
45 #include <QJsonValue>
46 #include <QFlags>
47 #include <QtDebug>
48 
49 #include "core/application.h"
50 #include "core/networkaccessmanager.h"
51 #include "core/song.h"
52 #include "core/timeconstants.h"
53 #include "core/logging.h"
54 #include "internet/localredirectserver.h"
55 #include "settings/scrobblersettingspage.h"
56 
57 #include "audioscrobbler.h"
58 #include "scrobblerservice.h"
59 #include "scrobblingapi20.h"
60 #include "scrobblercache.h"
61 #include "scrobblercacheitem.h"
62 
63 const char *ScrobblingAPI20::kApiKey = "211990b4c96782c05d1536e7219eb56e";
64 const char *ScrobblingAPI20::kSecret = "80fd738f49596e9709b1bf9319c444a8";
65 const int ScrobblingAPI20::kScrobblesPerRequest = 50;
66 
ScrobblingAPI20(const QString & name,const QString & settings_group,const QString & auth_url,const QString & api_url,const bool batch,Application * app,QObject * parent)67 ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, Application *app, QObject *parent)
68     : ScrobblerService(name, app, parent),
69       name_(name),
70       settings_group_(settings_group),
71       auth_url_(auth_url),
72       api_url_(api_url),
73       batch_(batch),
74       app_(app),
75       server_(nullptr),
76       enabled_(false),
77       https_(false),
78       prefer_albumartist_(false),
79       subscriber_(false),
80       submitted_(false),
81       scrobbled_(false),
82       timestamp_(0) {
83 
84   timer_submit_.setSingleShot(true);
85   QObject::connect(&timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit);
86 
87 }
88 
~ScrobblingAPI20()89 ScrobblingAPI20::~ScrobblingAPI20() {
90 
91   while (!replies_.isEmpty()) {
92     QNetworkReply *reply = replies_.takeFirst();
93     QObject::disconnect(reply, nullptr, this, nullptr);
94     reply->abort();
95     reply->deleteLater();
96   }
97 
98   if (server_) {
99     QObject::disconnect(server_, nullptr, this, nullptr);
100     if (server_->isListening()) server_->close();
101     server_->deleteLater();
102   }
103 
104 }
105 
ReloadSettings()106 void ScrobblingAPI20::ReloadSettings() {
107 
108   QSettings s;
109 
110   s.beginGroup(settings_group_);
111   enabled_ = s.value("enabled", false).toBool();
112   https_ = s.value("https", false).toBool();
113   s.endGroup();
114 
115   s.beginGroup(ScrobblerSettingsPage::kSettingsGroup);
116   prefer_albumartist_ = s.value("albumartist", false).toBool();
117   s.endGroup();
118 
119 }
120 
LoadSession()121 void ScrobblingAPI20::LoadSession() {
122 
123   QSettings s;
124   s.beginGroup(settings_group_);
125   subscriber_ = s.value("subscriber", false).toBool();
126   username_ = s.value("username").toString();
127   session_key_ = s.value("session_key").toString();
128   s.endGroup();
129 
130 }
131 
Logout()132 void ScrobblingAPI20::Logout() {
133 
134   subscriber_ = false;
135   username_.clear();
136   session_key_.clear();
137 
138   QSettings settings;
139   settings.beginGroup(settings_group_);
140   settings.remove("subscriber");
141   settings.remove("username");
142   settings.remove("session_key");
143   settings.endGroup();
144 
145 }
146 
Authenticate(const bool https)147 void ScrobblingAPI20::Authenticate(const bool https) {
148 
149   if (!server_) {
150     server_ = new LocalRedirectServer(this);
151     server_->set_https(https);
152     if (!server_->Listen()) {
153       AuthError(server_->error());
154       delete server_;
155       server_ = nullptr;
156       return;
157     }
158     QObject::connect(server_, &LocalRedirectServer::Finished, this, &ScrobblingAPI20::RedirectArrived);
159   }
160 
161   QUrlQuery url_query;
162   url_query.addQueryItem("api_key", kApiKey);
163   url_query.addQueryItem("cb", server_->url().toString());
164   QUrl url(auth_url_);
165   url.setQuery(url_query);
166 
167   QMessageBox messagebox(QMessageBox::Information, tr("%1 Scrobbler Authentication").arg(name_), tr("Open URL in web browser?") + QString("<br /><a href=\"%1\">%1</a><br />").arg(url.toString()) + tr("Press \"Save\" to copy the URL to clipboard and manually open it in a web browser."), QMessageBox::Open|QMessageBox::Save|QMessageBox::Cancel);
168   messagebox.setTextFormat(Qt::RichText);
169   int result = messagebox.exec();
170   switch (result) {
171   case QMessageBox::Open:{
172       bool openurl_result = QDesktopServices::openUrl(url);
173       if (openurl_result) {
174         break;
175       }
176       QMessageBox messagebox_error(QMessageBox::Warning, tr("%1 Scrobbler Authentication").arg(name_), tr("Could not open URL. Please open this URL in your browser") + QString(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
177       messagebox_error.setTextFormat(Qt::RichText);
178       messagebox_error.exec();
179     }
180     // fallthrough
181   case QMessageBox::Save:
182     QApplication::clipboard()->setText(url.toString());
183     break;
184   case QMessageBox::Cancel:
185     if (server_) {
186       server_->close();
187       server_->deleteLater();
188       server_ = nullptr;
189     }
190     emit AuthenticationComplete(false);
191     break;
192   default:
193     break;
194   }
195 
196 }
197 
RedirectArrived()198 void ScrobblingAPI20::RedirectArrived() {
199 
200   if (!server_) return;
201 
202   if (server_->error().isEmpty()) {
203     QUrl url = server_->request_url();
204     if (url.isValid()) {
205       QUrlQuery url_query(url);
206       if (url_query.hasQueryItem("token")) {
207         QString token = url_query.queryItemValue("token").toUtf8();
208         RequestSession(token);
209       }
210       else {
211         AuthError(tr("Invalid reply from web browser. Missing token."));
212       }
213     }
214     else {
215       AuthError(tr("Received invalid reply from web browser. Try the HTTPS option, or use another browser like Chromium or Chrome."));
216     }
217   }
218   else {
219     AuthError(server_->error());
220   }
221 
222   server_->close();
223   server_->deleteLater();
224   server_ = nullptr;
225 
226 }
227 
RequestSession(const QString & token)228 void ScrobblingAPI20::RequestSession(const QString &token) {
229 
230   QUrl session_url(api_url_);
231   QUrlQuery session_url_query;
232   session_url_query.addQueryItem("api_key", kApiKey);
233   session_url_query.addQueryItem("method", "auth.getSession");
234   session_url_query.addQueryItem("token", token);
235   QString data_to_sign;
236   for (const QPair<QString, QString> &param : session_url_query.queryItems()) {
237     data_to_sign += param.first + param.second;
238   }
239   data_to_sign += kSecret;
240   QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
241   QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
242   session_url_query.addQueryItem("api_sig", signature);
243   session_url_query.addQueryItem(QUrl::toPercentEncoding("format"), QUrl::toPercentEncoding("json"));
244   session_url.setQuery(session_url_query);
245 
246   QNetworkRequest req(session_url);
247 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
248   req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
249 #else
250   req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
251 #endif
252   QNetworkReply *reply = network()->get(req);
253   replies_ << reply;
254   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); });
255 
256 }
257 
AuthenticateReplyFinished(QNetworkReply * reply)258 void ScrobblingAPI20::AuthenticateReplyFinished(QNetworkReply *reply) {
259 
260   if (!replies_.contains(reply)) return;
261   replies_.removeAll(reply);
262   QObject::disconnect(reply, nullptr, this, nullptr);
263   reply->deleteLater();
264 
265   QByteArray data;
266 
267   if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
268     data = reply->readAll();
269   }
270   else {
271     if (reply->error() < 200) {
272       // This is a network error, there is nothing more to do.
273       AuthError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
274     }
275     else {
276       // See if there is Json data containing "error" and "message" - then use that instead.
277       data = reply->readAll();
278       QString error;
279       QJsonParseError json_error;
280       QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
281       if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
282         QJsonObject json_obj = json_doc.object();
283         if (json_obj.contains("error") && json_obj.contains("message")) {
284           int code = json_obj["error"].toInt();
285           QString message = json_obj["message"].toString();
286           error = "Error: " + QString::number(code) + ": " + message;
287         }
288         else {
289           error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
290         }
291       }
292       if (error.isEmpty()) {
293         if (reply->error() != QNetworkReply::NoError) {
294           error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
295         }
296         else {
297           error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
298         }
299       }
300       AuthError(error);
301     }
302     return;
303   }
304 
305   QJsonObject json_obj = ExtractJsonObj(data);
306   if (json_obj.isEmpty()) {
307     AuthError("Json document from server was empty.");
308     return;
309   }
310 
311   if (json_obj.contains("error") && json_obj.contains("message")) {
312     int error = json_obj["error"].toInt();
313     QString message = json_obj["message"].toString();
314     QString failure_reason = "Error: " + QString::number(error) + ": " + message;
315     AuthError(failure_reason);
316     return;
317   }
318 
319   if (!json_obj.contains("session")) {
320     AuthError("Json reply from server is missing session.");
321     return;
322   }
323 
324   QJsonValue json_session = json_obj["session"];
325   if (!json_session.isObject()) {
326     AuthError("Json session is not an object.");
327     return;
328   }
329   json_obj = json_session.toObject();
330   if (json_obj.isEmpty()) {
331     AuthError("Json session object is empty.");
332     return;
333   }
334   if (!json_obj.contains("subscriber") || !json_obj.contains("name") || !json_obj.contains("key")) {
335     AuthError("Json session object is missing values.");
336     return;
337   }
338 
339   subscriber_ = json_obj["subscriber"].toBool();
340   username_ = json_obj["name"].toString();
341   session_key_ = json_obj["key"].toString();
342 
343   QSettings s;
344   s.beginGroup(settings_group_);
345   s.setValue("subscriber", subscriber_);
346   s.setValue("username", username_);
347   s.setValue("session_key", session_key_);
348   s.endGroup();
349 
350   emit AuthenticationComplete(true);
351 
352   DoSubmit();
353 
354 }
355 
CreateRequest(const ParamList & request_params)356 QNetworkReply *ScrobblingAPI20::CreateRequest(const ParamList &request_params) {
357 
358   ParamList params = ParamList()
359     << Param("api_key", kApiKey)
360     << Param("sk", session_key_)
361     << Param("lang", QLocale().name().left(2).toLower())
362     << request_params;
363 
364   std::sort(params.begin(), params.end());
365 
366   QUrlQuery url_query;
367   QString data_to_sign;
368   for (const Param &param : params) {
369     EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
370     url_query.addQueryItem(encoded_param.first, encoded_param.second);
371     data_to_sign += param.first + param.second;
372   }
373   data_to_sign += kSecret;
374 
375   QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
376   QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower();
377 
378   url_query.addQueryItem("api_sig", QUrl::toPercentEncoding(signature));
379   url_query.addQueryItem("format", QUrl::toPercentEncoding("json"));
380 
381   QUrl url(api_url_);
382   QNetworkRequest req(url);
383 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
384   req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
385 #else
386   req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
387 #endif
388   req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
389   QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
390   QNetworkReply *reply = network()->post(req, query);
391   replies_ << reply;
392 
393   //qLog(Debug) << name_ << "Sending request" << url_query.toString(QUrl::FullyDecoded);
394 
395   return reply;
396 
397 }
398 
GetReplyData(QNetworkReply * reply)399 QByteArray ScrobblingAPI20::GetReplyData(QNetworkReply *reply) {
400 
401   QByteArray data;
402 
403   if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
404     data = reply->readAll();
405   }
406   else {
407     if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
408       // This is a network error, there is nothing more to do.
409       Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
410     }
411     else {
412       QString error;
413       // See if there is Json data containing "error" and "message" - then use that instead.
414       data = reply->readAll();
415       QJsonParseError json_error;
416       QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
417       int error_code = -1;
418       if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
419         QJsonObject json_obj = json_doc.object();
420         if (json_obj.contains("error") && json_obj.contains("message")) {
421           error_code = json_obj["error"].toInt();
422           QString error_message = json_obj["message"].toString();
423           error = QString("%1 (%2)").arg(error_message).arg(error_code);
424         }
425       }
426       if (error.isEmpty()) {
427         if (reply->error() != QNetworkReply::NoError) {
428           error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
429         }
430         else {
431           error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
432         }
433       }
434       if (reply->error() == QNetworkReply::ContentAccessDenied ||
435           reply->error() == QNetworkReply::ContentOperationNotPermittedError ||
436           reply->error() == QNetworkReply::AuthenticationRequiredError ||
437           error_code == ScrobbleErrorCode::InvalidSessionKey ||
438           error_code == ScrobbleErrorCode::UnauthorizedToken ||
439           error_code == ScrobbleErrorCode::LoginRequired ||
440           error_code == ScrobbleErrorCode::AuthenticationFailed ||
441           error_code == ScrobbleErrorCode::APIKeySuspended
442         ){
443         // Session is probably expired
444         Logout();
445       }
446       Error(error);
447     }
448     return QByteArray();
449   }
450 
451   return data;
452 
453 }
454 
UpdateNowPlaying(const Song & song)455 void ScrobblingAPI20::UpdateNowPlaying(const Song &song) {
456 
457   CheckScrobblePrevSong();
458 
459   song_playing_ = song;
460   timestamp_ = QDateTime::currentDateTime().toSecsSinceEpoch();
461   scrobbled_ = false;
462 
463   if (!IsAuthenticated() || !song.is_metadata_good() || app_->scrobbler()->IsOffline()) return;
464 
465   QString album = song.album();
466   QString title = song.title();
467 
468   album = album.remove(Song::kAlbumRemoveDisc);
469   album = album.remove(Song::kAlbumRemoveMisc);
470   title = title.remove(Song::kTitleRemoveMisc);
471 
472   ParamList params = ParamList()
473     << Param("method", "track.updateNowPlaying")
474     << Param("artist", prefer_albumartist_ && song.effective_albumartist() != Song::kVariousArtists ? song.effective_albumartist() : song.artist())
475     << Param("track", title);
476 
477   if (!album.isEmpty()) {
478     params << Param("album", album);
479   }
480 
481   if (!prefer_albumartist_ && !song.albumartist().isEmpty() && song.albumartist().compare(Song::kVariousArtists, Qt::CaseInsensitive) != 0) {
482     params << Param("albumArtist", song.albumartist());
483   }
484 
485   QNetworkReply *reply = CreateRequest(params);
486   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { UpdateNowPlayingRequestFinished(reply); });
487 
488 }
489 
UpdateNowPlayingRequestFinished(QNetworkReply * reply)490 void ScrobblingAPI20::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
491 
492   if (!replies_.contains(reply)) return;
493   replies_.removeAll(reply);
494   QObject::disconnect(reply, nullptr, this, nullptr);
495   reply->deleteLater();
496 
497   QByteArray data = GetReplyData(reply);
498   if (data.isEmpty()) {
499     return;
500   }
501 
502   QJsonObject json_obj = ExtractJsonObj(data);
503   if (json_obj.isEmpty()) {
504     return;
505   }
506 
507   if (json_obj.contains("error") && json_obj.contains("message")) {
508     int error_code = json_obj["error"].toInt();
509     QString error_message = json_obj["message"].toString();
510     QString error_reason = QString("%1 (%2)").arg(error_message).arg(error_code);
511     Error(error_reason);
512     return;
513   }
514 
515   if (!json_obj.contains("nowplaying")) {
516     Error("Json reply from server is missing nowplaying.", json_obj);
517     return;
518   }
519 
520 }
521 
ClearPlaying()522 void ScrobblingAPI20::ClearPlaying() {
523 
524   CheckScrobblePrevSong();
525 
526   song_playing_ = Song();
527   scrobbled_ = false;
528   timestamp_ = 0;
529 
530 }
531 
Scrobble(const Song & song)532 void ScrobblingAPI20::Scrobble(const Song &song) {
533 
534   if (song.id() != song_playing_.id() || song.url() != song_playing_.url() || !song.is_metadata_good()) return;
535 
536   scrobbled_ = true;
537 
538   cache()->Add(song, timestamp_);
539 
540   if (app_->scrobbler()->IsOffline()) return;
541 
542   if (!IsAuthenticated()) {
543     if (app_->scrobbler()->ShowErrorDialog()) { emit ErrorMessage(tr("Scrobbler %1 is not authenticated!").arg(name_)); }
544     return;
545   }
546 
547   if (!submitted_) {
548     submitted_ = true;
549     if (!batch_ || app_->scrobbler()->SubmitDelay() <= 0) {
550       Submit();
551     }
552     else if (!timer_submit_.isActive()) {
553       timer_submit_.setInterval(static_cast<int>(app_->scrobbler()->SubmitDelay() * kMsecPerSec));
554       timer_submit_.start();
555     }
556   }
557 
558 }
559 
DoSubmit()560 void ScrobblingAPI20::DoSubmit() {
561 
562   if (!submitted_ && cache()->Count() > 0) {
563     submitted_ = true;
564     if (!timer_submit_.isActive()) {
565       timer_submit_.setInterval(static_cast<int>(app_->scrobbler()->SubmitDelay() * kMsecPerSec));
566       timer_submit_.start();
567     }
568   }
569 
570 }
571 
Submit()572 void ScrobblingAPI20::Submit() {
573 
574   submitted_ = false;
575 
576   if (!IsEnabled() || !IsAuthenticated() || app_->scrobbler()->IsOffline()) return;
577 
578   qLog(Debug) << name_ << "Submitting scrobbles.";
579 
580   ParamList params = ParamList() << Param("method", "track.scrobble");
581 
582   int i = 0;
583   QList<quint64> list;
584   QList<ScrobblerCacheItemPtr> items = cache()->List();
585   for (ScrobblerCacheItemPtr item : items) {  // clazy:exclude=range-loop-reference
586     if (item->sent_) continue;
587     item->sent_ = true;
588     if (!batch_) {
589       SendSingleScrobble(item);
590       continue;
591     }
592     list << item->timestamp_;
593     params << Param(QString("%1[%2]").arg("artist").arg(i), prefer_albumartist_ ? item->effective_albumartist() : item->artist_);
594     params << Param(QString("%1[%2]").arg("track").arg(i), item->song_);
595     params << Param(QString("%1[%2]").arg("timestamp").arg(i), QString::number(item->timestamp_));
596     params << Param(QString("%1[%2]").arg("duration").arg(i), QString::number(item->duration_ / kNsecPerSec));
597     if (!item->album_.isEmpty()) {
598       params << Param(QString("%1[%2]").arg("album").arg(i), item->album_);
599     }
600     if (!prefer_albumartist_ && !item->albumartist_.isEmpty() && item->albumartist_.compare(Song::kVariousArtists, Qt::CaseInsensitive) != 0) {
601       params << Param(QString("%1[%2]").arg("albumArtist").arg(i), item->albumartist_);
602     }
603     if (item->track_ > 0) {
604       params << Param(QString("%1[%2]").arg("trackNumber").arg(i), QString::number(item->track_));
605     }
606     ++i;
607     if (i >= kScrobblesPerRequest) break;
608   }
609 
610   if (!batch_ || i <= 0) return;
611 
612   QNetworkReply *reply = CreateRequest(params);
613   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, list]() { ScrobbleRequestFinished(reply, list); });
614 
615 }
616 
ScrobbleRequestFinished(QNetworkReply * reply,const QList<quint64> & list)617 void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, const QList<quint64> &list) {
618 
619   if (!replies_.contains(reply)) return;
620   replies_.removeAll(reply);
621   QObject::disconnect(reply, nullptr, this, nullptr);
622   reply->deleteLater();
623 
624   QByteArray data = GetReplyData(reply);
625   if (data.isEmpty()) {
626     cache()->ClearSent(list);
627     DoSubmit();
628     return;
629   }
630 
631   QJsonObject json_obj = ExtractJsonObj(data);
632   if (json_obj.isEmpty()) {
633     cache()->ClearSent(list);
634     DoSubmit();
635     return;
636   }
637 
638   if (json_obj.contains("error") && json_obj.contains("message")) {
639     int error_code = json_obj["error"].toInt();
640     QString error_message = json_obj["message"].toString();
641     QString error_reason = QString("%1 (%2)").arg(error_message).arg(error_code);
642     Error(error_reason);
643     cache()->ClearSent(list);
644     DoSubmit();
645     return;
646   }
647 
648   if (!json_obj.contains("scrobbles")) {
649     Error("Json reply from server is missing scrobbles.", json_obj);
650     cache()->ClearSent(list);
651     DoSubmit();
652     return;
653   }
654 
655   cache()->Flush(list);
656 
657   QJsonValue value_scrobbles = json_obj["scrobbles"];
658   if (!value_scrobbles.isObject()) {
659     Error("Json scrobbles is not an object.", json_obj);
660     DoSubmit();
661     return;
662   }
663   json_obj = value_scrobbles.toObject();
664   if (json_obj.isEmpty()) {
665     Error("Json scrobbles object is empty.", value_scrobbles);
666     DoSubmit();
667     return;
668   }
669   if (!json_obj.contains("@attr") || !json_obj.contains("scrobble")) {
670     Error("Json scrobbles object is missing values.", json_obj);
671     DoSubmit();
672     return;
673   }
674 
675   QJsonValue value_attr = json_obj["@attr"];
676   if (!value_attr.isObject()) {
677     Error("Json scrobbles attr is not an object.", value_attr);
678     DoSubmit();
679     return;
680   }
681   QJsonObject obj_attr = value_attr.toObject();
682   if (obj_attr.isEmpty()) {
683     Error("Json scrobbles attr is empty.", value_attr);
684     DoSubmit();
685     return;
686   }
687   if (!obj_attr.contains("accepted") || !obj_attr.contains("ignored")) {
688     Error("Json scrobbles attr is missing values.", obj_attr);
689     DoSubmit();
690     return;
691   }
692   int accepted = obj_attr["accepted"].toInt();
693   int ignored = obj_attr["ignored"].toInt();
694 
695   qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
696 
697   QJsonArray array_scrobble;
698 
699   QJsonValue value_scrobble = json_obj["scrobble"];
700   if (value_scrobble.isObject()) {
701     QJsonObject obj_scrobble = value_scrobble.toObject();
702     if (obj_scrobble.isEmpty()) {
703       Error("Json scrobbles scrobble object is empty.", obj_scrobble);
704       DoSubmit();
705       return;
706     }
707     array_scrobble.append(obj_scrobble);
708   }
709   else if (value_scrobble.isArray()) {
710     array_scrobble = value_scrobble.toArray();
711     if (array_scrobble.isEmpty()) {
712       Error("Json scrobbles scrobble array is empty.", value_scrobble);
713       DoSubmit();
714       return;
715     }
716   }
717   else {
718     Error("Json scrobbles scrobble is not an object or array.", value_scrobble);
719     DoSubmit();
720     return;
721   }
722 
723   for (const QJsonValueRef value : array_scrobble) {  // clazy:exclude=range-loop
724 
725     if (!value.isObject()) {
726       Error("Json scrobbles scrobble array value is not an object.");
727       continue;
728     }
729     QJsonObject json_track = value.toObject();
730     if (json_track.isEmpty()) {
731       continue;
732     }
733 
734     if (!json_track.contains("artist") ||
735         !json_track.contains("album") ||
736         !json_track.contains("albumArtist") ||
737         !json_track.contains("track") ||
738         !json_track.contains("timestamp") ||
739         !json_track.contains("ignoredMessage")
740     ) {
741       Error("Json scrobbles scrobble is missing values.", json_track);
742       continue;
743     }
744 
745     QJsonValue value_artist = json_track["artist"];
746     QJsonValue value_album = json_track["album"];
747     QJsonValue value_song = json_track["track"];
748     QJsonValue value_ignoredmessage = json_track["ignoredMessage"];
749     //quint64 timestamp = json_track["timestamp"].toVariant().toULongLong();
750 
751     if (!value_artist.isObject() || !value_album.isObject() || !value_song.isObject() || !value_ignoredmessage.isObject()) {
752       Error("Json scrobbles scrobble values are not objects.", json_track);
753       continue;
754     }
755 
756     QJsonObject obj_artist = value_artist.toObject();
757     QJsonObject obj_album = value_album.toObject();
758     QJsonObject obj_song = value_song.toObject();
759     QJsonObject obj_ignoredmessage = value_ignoredmessage.toObject();
760 
761     if (obj_artist.isEmpty() || obj_album.isEmpty() || obj_song.isEmpty() || obj_ignoredmessage.isEmpty()) {
762       Error("Json scrobbles scrobble values objects are empty.", json_track);
763       continue;
764     }
765 
766     if (!obj_artist.contains("#text") || !obj_album.contains("#text") || !obj_song.contains("#text")) {
767       continue;
768     }
769 
770     //QString artist = obj_artist["#text"].toString();
771     //QString album = obj_album["#text"].toString();
772     QString song = obj_song["#text"].toString();
773     bool ignoredmessage = obj_ignoredmessage["code"].toVariant().toBool();
774     QString ignoredmessage_text = obj_ignoredmessage["#text"].toString();
775 
776     if (ignoredmessage) {
777       Error(QString("Scrobble for \"%1\" ignored: %2").arg(song, ignoredmessage_text));
778     }
779     else {
780       qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
781     }
782 
783  }
784 
785   DoSubmit();
786 
787 }
788 
SendSingleScrobble(ScrobblerCacheItemPtr item)789 void ScrobblingAPI20::SendSingleScrobble(ScrobblerCacheItemPtr item) {
790 
791   ParamList params = ParamList()
792     << Param("method", "track.scrobble")
793     << Param("artist", prefer_albumartist_ ? item->effective_albumartist() : item->artist_)
794     << Param("track", item->song_)
795     << Param("timestamp", QString::number(item->timestamp_))
796     << Param("duration", QString::number(item->duration_ / kNsecPerSec));
797 
798   if (!item->album_.isEmpty()) {
799     params << Param("album", item->album_);
800   }
801   if (!prefer_albumartist_ && !item->albumartist_.isEmpty() && item->albumartist_.compare(Song::kVariousArtists, Qt::CaseInsensitive) != 0) {
802     params << Param("albumArtist", item->albumartist_);
803   }
804   if (item->track_ > 0) {
805     params << Param("trackNumber", QString::number(item->track_));
806   }
807 
808   QNetworkReply *reply = CreateRequest(params);
809   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, item]() { SingleScrobbleRequestFinished(reply, item->timestamp_); });
810 
811 }
812 
SingleScrobbleRequestFinished(QNetworkReply * reply,const quint64 timestamp)813 void ScrobblingAPI20::SingleScrobbleRequestFinished(QNetworkReply *reply, const quint64 timestamp) {
814 
815   if (!replies_.contains(reply)) return;
816   replies_.removeAll(reply);
817   QObject::disconnect(reply, nullptr, this, nullptr);
818   reply->deleteLater();
819 
820   ScrobblerCacheItemPtr item = cache()->Get(timestamp);
821   if (!item) {
822     Error(QString("Received reply for non-existing cache entry %1.").arg(timestamp));
823     return;
824   }
825 
826   QByteArray data = GetReplyData(reply);
827   if (data.isEmpty()) {
828     item->sent_ = false;
829     return;
830   }
831 
832   QJsonObject json_obj = ExtractJsonObj(data);
833   if (json_obj.isEmpty()) {
834     item->sent_ = false;
835     return;
836   }
837 
838   if (json_obj.contains("error") && json_obj.contains("message")) {
839     int error_code = json_obj["error"].toInt();
840     QString error_message = json_obj["message"].toString();
841     QString error_reason = QString("%1 (%2)").arg(error_message).arg(error_code);
842     Error(error_reason);
843     item->sent_ = false;
844     return;
845   }
846 
847   if (!json_obj.contains("scrobbles")) {
848     Error("Json reply from server is missing scrobbles.", json_obj);
849     item->sent_ = false;
850     return;
851   }
852 
853   cache()->Remove(timestamp);
854   item = nullptr;
855 
856   QJsonValue value_scrobbles = json_obj["scrobbles"];
857   if (!value_scrobbles.isObject()) {
858     Error("Json scrobbles is not an object.", json_obj);
859     return;
860   }
861   json_obj = value_scrobbles.toObject();
862   if (json_obj.isEmpty()) {
863     Error("Json scrobbles object is empty.", value_scrobbles);
864     return;
865   }
866   if (!json_obj.contains("@attr") || !json_obj.contains("scrobble")) {
867     Error("Json scrobbles object is missing values.", json_obj);
868     return;
869   }
870 
871   QJsonValue value_attr = json_obj["@attr"];
872   if (!value_attr.isObject()) {
873     Error("Json scrobbles attr is not an object.", value_attr);
874     return;
875   }
876   QJsonObject obj_attr = value_attr.toObject();
877   if (obj_attr.isEmpty()) {
878     Error("Json scrobbles attr is empty.", value_attr);
879     return;
880   }
881 
882   QJsonValue value_scrobble = json_obj["scrobble"];
883   if (!value_scrobble.isObject()) {
884     Error("Json scrobbles scrobble is not an object.", value_scrobble);
885     return;
886   }
887   QJsonObject json_obj_scrobble = value_scrobble.toObject();
888   if (json_obj_scrobble.isEmpty()) {
889     Error("Json scrobbles scrobble is empty.", value_scrobble);
890     return;
891   }
892 
893   if (!obj_attr.contains("accepted") || !obj_attr.contains("ignored")) {
894     Error("Json scrobbles attr is missing values.", obj_attr);
895     return;
896   }
897 
898   if (!json_obj_scrobble.contains("artist") || !json_obj_scrobble.contains("album") || !json_obj_scrobble.contains("albumArtist") || !json_obj_scrobble.contains("track") || !json_obj_scrobble.contains("timestamp")) {
899     Error("Json scrobbles scrobble is missing values.", json_obj_scrobble);
900     return;
901   }
902 
903   QJsonValue json_value_artist = json_obj_scrobble["artist"];
904   QJsonValue json_value_album = json_obj_scrobble["album"];
905   QJsonValue json_value_song = json_obj_scrobble["track"];
906 
907   if (!json_value_artist.isObject() || !json_value_album.isObject() || !json_value_song.isObject()) {
908     Error("Json scrobbles scrobble values are not objects.", json_obj_scrobble);
909     return;
910   }
911 
912   QJsonObject json_obj_artist = json_value_artist.toObject();
913   QJsonObject json_obj_album = json_value_album.toObject();
914   QJsonObject json_obj_song = json_value_song.toObject();
915 
916   if (json_obj_artist.isEmpty() || json_obj_album.isEmpty() || json_obj_song.isEmpty()) {
917     Error("Json scrobbles scrobble values objects are empty.", json_obj_scrobble);
918     return;
919   }
920 
921   if (!json_obj_artist.contains("#text") || !json_obj_album.contains("#text") || !json_obj_song.contains("#text")) {
922     Error("Json scrobbles scrobble values objects are missing #text.", json_obj_artist);
923     return;
924   }
925 
926   //QString artist = json_obj_artist["#text"].toString();
927   //QString album = json_obj_album["#text"].toString();
928   QString song = json_obj_song["#text"].toString();
929 
930   int accepted = obj_attr["accepted"].toVariant().toInt();
931   if (accepted == 1) {
932     qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
933   }
934   else {
935     Error(QString("Scrobble for \"%1\" not accepted").arg(song));
936   }
937 
938 }
939 
Love()940 void ScrobblingAPI20::Love() {
941 
942   if (!song_playing_.is_valid() || !song_playing_.is_metadata_good()) return;
943 
944   if (!IsAuthenticated()) app_->scrobbler()->ShowConfig();
945 
946   qLog(Debug) << name_ << "Sending love for song" << song_playing_.artist() << song_playing_.album() << song_playing_.title();
947 
948   ParamList params = ParamList()
949     << Param("method", "track.love")
950     << Param("artist", prefer_albumartist_ && song_playing_.effective_albumartist() != Song::kVariousArtists ? song_playing_.effective_albumartist() : song_playing_.artist())
951     << Param("track", song_playing_.title());
952 
953   if (!song_playing_.album().isEmpty())
954     params << Param("album", song_playing_.album());
955 
956   if (!prefer_albumartist_ && !song_playing_.albumartist().isEmpty() && song_playing_.albumartist().compare(Song::kVariousArtists, Qt::CaseInsensitive) != 0)
957     params << Param("albumArtist", song_playing_.albumartist());
958 
959   QNetworkReply *reply = CreateRequest(params);
960   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] { LoveRequestFinished(reply); });
961 
962 }
963 
LoveRequestFinished(QNetworkReply * reply)964 void ScrobblingAPI20::LoveRequestFinished(QNetworkReply *reply) {
965 
966   if (!replies_.contains(reply)) return;
967   replies_.removeAll(reply);
968   QObject::disconnect(reply, nullptr, this, nullptr);
969   reply->deleteLater();
970 
971   QByteArray data = GetReplyData(reply);
972   if (data.isEmpty()) {
973     return;
974   }
975 
976   QJsonObject json_obj = ExtractJsonObj(data, true);
977   if (json_obj.isEmpty()) {
978     return;
979   }
980 
981   if (json_obj.contains("error")) {
982     QJsonValue json_value = json_obj["error"];
983     if (!json_value.isObject()) {
984       Error("Error is not on object.");
985       return;
986     }
987     QJsonObject json_obj_error = json_value.toObject();
988     if (json_obj_error.isEmpty()) {
989       Error("Received empty json error object.", json_obj);
990       return;
991     }
992     if (json_obj_error.contains("code") && json_obj_error.contains("#text")) {
993       int code = json_obj_error["code"].toInt();
994       QString text = json_obj_error["#text"].toString();
995       QString error_reason = QString("%1 (%2)").arg(text).arg(code);
996       Error(error_reason);
997       return;
998     }
999   }
1000 
1001   if (json_obj.contains("lfm")) {
1002     QJsonValue json_value = json_obj["lfm"];
1003     if (json_value.isObject()) {
1004       QJsonObject json_obj_lfm = json_value.toObject();
1005       if (json_obj_lfm.contains("status")) {
1006         QString status = json_obj_lfm["status"].toString();
1007         qLog(Debug) << name_ << "Received love status:" << status;
1008         return;
1009       }
1010     }
1011   }
1012 
1013 }
1014 
AuthError(const QString & error)1015 void ScrobblingAPI20::AuthError(const QString &error) {
1016   emit AuthenticationComplete(false, error);
1017 }
1018 
Error(const QString & error,const QVariant & debug)1019 void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) {
1020 
1021   qLog(Error) << name_ << error;
1022   if (debug.isValid()) qLog(Debug) << debug;
1023 
1024   if (app_->scrobbler()->ShowErrorDialog()) { emit ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error)); }
1025 
1026 }
1027 
ErrorString(const ScrobbleErrorCode error)1028 QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) {
1029 
1030   switch (error) {
1031     case ScrobbleErrorCode::NoError:
1032       return QString("This error does not exist.");
1033     case ScrobbleErrorCode::InvalidService:
1034       return QString("Invalid service - This service does not exist.");
1035     case ScrobbleErrorCode::InvalidMethod:
1036       return QString("Invalid Method - No method with that name in this package.");
1037     case ScrobbleErrorCode::AuthenticationFailed:
1038       return QString("Authentication Failed - You do not have permissions to access the service.");
1039     case ScrobbleErrorCode::InvalidFormat:
1040       return QString("Invalid format - This service doesn't exist in that format.");
1041     case ScrobbleErrorCode::InvalidParameters:
1042       return QString("Invalid parameters - Your request is missing a required parameter.");
1043     case ScrobbleErrorCode::InvalidResourceSpecified:
1044       return QString("Invalid resource specified");
1045     case ScrobbleErrorCode::OperationFailed:
1046       return QString("Operation failed - Most likely the backend service failed. Please try again.");
1047     case ScrobbleErrorCode::InvalidSessionKey:
1048       return QString("Invalid session key - Please re-authenticate.");
1049     case ScrobbleErrorCode::InvalidApiKey:
1050       return QString("Invalid API key - You must be granted a valid key by last.fm.");
1051     case ScrobbleErrorCode::ServiceOffline:
1052       return QString("Service Offline - This service is temporarily offline. Try again later.");
1053     case ScrobbleErrorCode::SubscribersOnly:
1054       return QString("Subscribers Only - This station is only available to paid last.fm subscribers.");
1055     case ScrobbleErrorCode::InvalidMethodSignature:
1056       return QString("Invalid method signature supplied.");
1057     case ScrobbleErrorCode::UnauthorizedToken:
1058       return QString("Unauthorized Token - This token has not been authorized.");
1059     case ScrobbleErrorCode::ItemUnavailable:
1060       return QString("This item is not available for streaming.");
1061     case ScrobbleErrorCode::TemporarilyUnavailable:
1062       return QString("The service is temporarily unavailable, please try again.");
1063     case ScrobbleErrorCode::LoginRequired:
1064       return QString("Login: User requires to be logged in.");
1065     case ScrobbleErrorCode::TrialExpired:
1066       return QString("Trial Expired - This user has no free radio plays left. Subscription required.");
1067     case ScrobbleErrorCode::ErrorDoesNotExist:
1068       return QString("This error does not exist.");
1069     case ScrobbleErrorCode::NotEnoughContent:
1070       return QString("Not Enough Content - There is not enough content to play this station.");
1071     case ScrobbleErrorCode::NotEnoughMembers:
1072       return QString("Not Enough Members - This group does not have enough members for radio.");
1073     case ScrobbleErrorCode::NotEnoughFans:
1074       return QString("Not Enough Fans - This artist does not have enough fans for for radio.");
1075     case ScrobbleErrorCode::NotEnoughNeighbours:
1076       return QString("Not Enough Neighbours - There are not enough neighbours for radio.");
1077     case ScrobbleErrorCode::NoPeakRadio:
1078       return QString("No Peak Radio - This user is not allowed to listen to radio during peak usage.");
1079     case ScrobbleErrorCode::RadioNotFound:
1080       return QString("Radio Not Found - Radio station not found.");
1081     case ScrobbleErrorCode::APIKeySuspended:
1082       return QString("Suspended API key - Access for your account has been suspended, please contact Last.fm");
1083     case ScrobbleErrorCode::Deprecated:
1084       return QString("Deprecated - This type of request is no longer supported.");
1085     case ScrobbleErrorCode::RateLimitExceeded:
1086       return QString("Rate limit exceeded - Your IP has made too many requests in a short period.");
1087   }
1088 
1089   return QString("Unknown error.");
1090 
1091 }
1092 
CheckScrobblePrevSong()1093 void ScrobblingAPI20::CheckScrobblePrevSong() {
1094 
1095   quint64 duration = QDateTime::currentDateTime().toSecsSinceEpoch() - timestamp_;
1096 
1097   if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) {
1098     Song song(song_playing_);
1099     song.set_length_nanosec(duration * kNsecPerSec);
1100     Scrobble(song);
1101   }
1102 
1103 }
1104