Scrobbler: Refactor

This commit is contained in:
Jonas Kvinge
2025-03-08 23:19:42 +01:00
parent cd516c37b9
commit b02b114caf
18 changed files with 508 additions and 800 deletions

View File

@@ -3,7 +3,7 @@
* This file was part of Clementine. * This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com> * Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com> * Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -208,7 +208,7 @@ class ApplicationImpl {
scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network())); scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network()));
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network())); scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
#ifdef HAVE_SUBSONIC #ifdef HAVE_SUBSONIC
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->streaming_services()->Service<SubsonicService>(), app)); scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
#endif #endif
return scrobbler; return scrobbler;
}), }),

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2020-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2020-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -55,7 +55,7 @@ constexpr int kRequestsDelay = 2000;
} }
LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent) LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent), : JsonBaseRequest(network, parent),
network_(network), network_(network),
timer_flush_requests_(new QTimer(this)), timer_flush_requests_(new QTimer(this)),
lastplayed_(false), lastplayed_(false),
@@ -117,19 +117,7 @@ QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
std::sort(params.begin(), params.end()); std::sort(params.begin(), params.end());
QUrlQuery url_query; QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(LastFMScrobbler::kApiUrl)), params);
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(LastFMScrobbler::kApiUrl));
url.setQuery(url_query);
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
QNetworkReply *reply = network_->get(req);
replies_ << reply;
//qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded); //qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded);
@@ -137,73 +125,52 @@ QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
} }
QByteArray LastFMImport::GetReplyData(QNetworkReply *reply) { JsonBaseRequest::JsonObjectResult LastFMImport::ParseJsonObject(QNetworkReply *reply) {
QByteArray data; if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
} }
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { JsonObjectResult result(ErrorCode::Success);
// This is a network error, there is nothing more to do. result.network_error = reply->error();
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error())); if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("error"_L1) && json_object.contains("message"_L1)) {
const int error = json_object["error"_L1].toInt();
const QString message = json_object["message"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(error);
}
else {
result.json_object = json_document.object();
}
} }
else { else {
QString error; result.error_code = ErrorCode::ParseError;
// See if there is Json data containing "error" and "message" - then use that instead. result.error_message = json_parse_error.errorString();
data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) {
int error_code = json_obj["error"_L1].toInt();
QString error_message = json_obj["message"_L1].toString();
error = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
} }
return QByteArray();
} }
return data; if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
} result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
QJsonObject LastFMImport::ExtractJsonObj(const QByteArray &data) { }
else if (result.http_status_code != 200) {
QJsonParseError error; result.error_code = ErrorCode::HttpError;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
}
if (error.error != QJsonParseError::NoError) {
Error(u"Reply from server missing Json data."_s, data);
return QJsonObject();
}
if (json_doc.isEmpty()) {
Error(u"Received empty Json document."_s, json_doc);
return QJsonObject();
}
if (!json_doc.isObject()) {
Error(u"Json document is not an object."_s, json_doc);
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(u"Received empty Json object."_s, json_doc);
return QJsonObject();
} }
return json_obj; return result;
} }
@@ -279,72 +246,65 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QByteArray data = GetReplyData(reply); const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (data.isEmpty()) { if (!json_object_result.success()) {
Error(json_object_result.error_message);
return; return;
} }
QJsonObject json_obj = ExtractJsonObj(data); QJsonObject json_object = json_object_result.json_object;
if (json_obj.isEmpty()) { if (json_object.isEmpty()) {
return; return;
} }
if (json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) { if (!json_object.contains("recenttracks"_L1)) {
int error_code = json_obj["error"_L1].toInt(); Error(u"JSON reply from server is missing recenttracks."_s, json_object);
QString error_message = json_obj["message"_L1].toString();
QString error_reason = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
Error(error_reason);
return; return;
} }
if (!json_obj.contains("recenttracks"_L1)) { if (!json_object["recenttracks"_L1].isObject()) {
Error(u"JSON reply from server is missing recenttracks."_s, json_obj); Error(u"Failed to parse JSON: recenttracks is not an object!"_s, json_object);
return;
}
json_object = json_object["recenttracks"_L1].toObject();
if (!json_object.contains("@attr"_L1)) {
Error(u"JSON reply from server is missing @attr."_s, json_object);
return; return;
} }
if (!json_obj["recenttracks"_L1].isObject()) { if (!json_object.contains("track"_L1)) {
Error(u"Failed to parse JSON: recenttracks is not an object!"_s, json_obj); Error(u"JSON reply from server is missing track."_s, json_object);
return;
}
json_obj = json_obj["recenttracks"_L1].toObject();
if (!json_obj.contains("@attr"_L1)) {
Error(u"JSON reply from server is missing @attr."_s, json_obj);
return; return;
} }
if (!json_obj.contains("track"_L1)) { if (!json_object["@attr"_L1].isObject()) {
Error(u"JSON reply from server is missing track."_s, json_obj); Error(u"Failed to parse JSON: @attr is not an object."_s, json_object);
return; return;
} }
if (!json_obj["@attr"_L1].isObject()) { if (!json_object["track"_L1].isArray()) {
Error(u"Failed to parse JSON: @attr is not an object."_s, json_obj); Error(u"Failed to parse JSON: track is not an object."_s, json_object);
return; return;
} }
if (!json_obj["track"_L1].isArray()) { const QJsonObject obj_attr = json_object["@attr"_L1].toObject();
Error(u"Failed to parse JSON: track is not an object."_s, json_obj);
return;
}
QJsonObject obj_attr = json_obj["@attr"_L1].toObject();
if (!obj_attr.contains("page"_L1)) { if (!obj_attr.contains("page"_L1)) {
Error(u"Failed to parse JSON: attr object is missing page."_s, json_obj); Error(u"Failed to parse JSON: attr object is missing page."_s, json_object);
return; return;
} }
if (!obj_attr.contains("totalPages"_L1)) { if (!obj_attr.contains("totalPages"_L1)) {
Error(u"Failed to parse JSON: attr object is missing totalPages."_s, json_obj); Error(u"Failed to parse JSON: attr object is missing totalPages."_s, json_object);
return; return;
} }
if (!obj_attr.contains("total"_L1)) { if (!obj_attr.contains("total"_L1)) {
Error(u"Failed to parse JSON: attr object is missing total."_s, json_obj); Error(u"Failed to parse JSON: attr object is missing total."_s, json_object);
return; return;
} }
int total = obj_attr["total"_L1].toString().toInt(); const int total = obj_attr["total"_L1].toString().toInt();
int pages = obj_attr["totalPages"_L1].toString().toInt(); const int pages = obj_attr["totalPages"_L1].toString().toInt();
if (page == 0) { if (page == 0) {
lastplayed_total_ = total; lastplayed_total_ = total;
@@ -353,7 +313,7 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
} }
else { else {
const QJsonArray array_track = json_obj["track"_L1].toArray(); const QJsonArray array_track = json_object["track"_L1].toArray();
for (const QJsonValue &value_track : array_track) { for (const QJsonValue &value_track : array_track) {
@@ -374,19 +334,19 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
continue; continue;
} }
QJsonObject obj_artist = obj_track["artist"_L1].toObject(); const QJsonObject obj_artist = obj_track["artist"_L1].toObject();
QJsonObject obj_album = obj_track["album"_L1].toObject(); const QJsonObject obj_album = obj_track["album"_L1].toObject();
QJsonObject obj_date = obj_track["date"_L1].toObject(); const QJsonObject obj_date = obj_track["date"_L1].toObject();
if (!obj_artist.contains("#text"_L1) || !obj_album.contains("#text"_L1) || !obj_date.contains("#text"_L1)) { if (!obj_artist.contains("#text"_L1) || !obj_album.contains("#text"_L1) || !obj_date.contains("#text"_L1)) {
continue; continue;
} }
QString artist = obj_artist["#text"_L1].toString(); const QString artist = obj_artist["#text"_L1].toString();
QString album = obj_album["#text"_L1].toString(); const QString album = obj_album["#text"_L1].toString();
QString date = obj_date["#text"_L1].toString(); const QString date = obj_date["#text"_L1].toString();
QString title = obj_track["name"_L1].toString(); const QString title = obj_track["name"_L1].toString();
QDateTime datetime = QDateTime::fromString(date, u"dd MMM yyyy, hh:mm"_s); const QDateTime datetime = QDateTime::fromString(date, u"dd MMM yyyy, hh:mm"_s);
if (datetime.isValid()) { if (datetime.isValid()) {
Q_EMIT UpdateLastPlayed(artist, album, title, datetime.toSecsSinceEpoch()); Q_EMIT UpdateLastPlayed(artist, album, title, datetime.toSecsSinceEpoch());
} }
@@ -442,72 +402,65 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QByteArray data = GetReplyData(reply); const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (data.isEmpty()) { if (!json_object_result.success()) {
Error(json_object_result.error_message);
return; return;
} }
QJsonObject json_obj = ExtractJsonObj(data); QJsonObject json_object = json_object_result.json_object;
if (json_obj.isEmpty()) { if (json_object.isEmpty()) {
return; return;
} }
if (json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) { if (!json_object.contains("toptracks"_L1)) {
int error_code = json_obj["error"_L1].toInt(); Error(u"JSON reply from server is missing toptracks."_s, json_object);
QString error_message = json_obj["message"_L1].toString();
QString error_reason = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
Error(error_reason);
return; return;
} }
if (!json_obj.contains("toptracks"_L1)) { if (!json_object["toptracks"_L1].isObject()) {
Error(u"JSON reply from server is missing toptracks."_s, json_obj); Error(u"Failed to parse JSON: toptracks is not an object!"_s, json_object);
return;
}
json_object = json_object["toptracks"_L1].toObject();
if (!json_object.contains("@attr"_L1)) {
Error(u"JSON reply from server is missing @attr."_s, json_object);
return; return;
} }
if (!json_obj["toptracks"_L1].isObject()) { if (!json_object.contains("track"_L1)) {
Error(u"Failed to parse JSON: toptracks is not an object!"_s, json_obj); Error(u"JSON reply from server is missing track."_s, json_object);
return;
}
json_obj = json_obj["toptracks"_L1].toObject();
if (!json_obj.contains("@attr"_L1)) {
Error(u"JSON reply from server is missing @attr."_s, json_obj);
return; return;
} }
if (!json_obj.contains("track"_L1)) { if (!json_object["@attr"_L1].isObject()) {
Error(u"JSON reply from server is missing track."_s, json_obj); Error(u"Failed to parse JSON: @attr is not an object."_s, json_object);
return; return;
} }
if (!json_obj["@attr"_L1].isObject()) { if (!json_object["track"_L1].isArray()) {
Error(u"Failed to parse JSON: @attr is not an object."_s, json_obj); Error(u"Failed to parse JSON: track is not an object."_s, json_object);
return; return;
} }
if (!json_obj["track"_L1].isArray()) { const QJsonObject object_attr = json_object["@attr"_L1].toObject();
Error(u"Failed to parse JSON: track is not an object."_s, json_obj);
if (!object_attr.contains("page"_L1)) {
Error(u"Failed to parse JSON: attr object is missing page."_s, json_object);
return;
}
if (!object_attr.contains("totalPages"_L1)) {
Error(u"Failed to parse JSON: attr object is missing page."_s, json_object);
return;
}
if (!object_attr.contains("total"_L1)) {
Error(u"Failed to parse JSON: attr object is missing total."_s, json_object);
return; return;
} }
QJsonObject obj_attr = json_obj["@attr"_L1].toObject(); const int pages = object_attr["totalPages"_L1].toString().toInt();
const int total = object_attr["total"_L1].toString().toInt();
if (!obj_attr.contains("page"_L1)) {
Error(u"Failed to parse JSON: attr object is missing page."_s, json_obj);
return;
}
if (!obj_attr.contains("totalPages"_L1)) {
Error(u"Failed to parse JSON: attr object is missing page."_s, json_obj);
return;
}
if (!obj_attr.contains("total"_L1)) {
Error(u"Failed to parse JSON: attr object is missing total."_s, json_obj);
return;
}
int pages = obj_attr["totalPages"_L1].toString().toInt();
int total = obj_attr["total"_L1].toString().toInt();
if (page == 0) { if (page == 0) {
playcount_total_ = total; playcount_total_ = total;
@@ -516,8 +469,8 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
} }
else { else {
QJsonArray array_track = json_obj["track"_L1].toArray(); const QJsonArray array_track = json_object["track"_L1].toArray();
for (QJsonArray::iterator it = array_track.begin(); it != array_track.end(); ++it) { for (QJsonArray::ConstIterator it = array_track.begin(); it != array_track.constEnd(); ++it) {
const QJsonValue &value_track = *it; const QJsonValue &value_track = *it;
@@ -527,7 +480,7 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
continue; continue;
} }
QJsonObject obj_track = value_track.toObject(); const QJsonObject obj_track = value_track.toObject();
if (!obj_track.contains("artist"_L1) || if (!obj_track.contains("artist"_L1) ||
!obj_track.contains("name"_L1) || !obj_track.contains("name"_L1) ||
!obj_track.contains("playcount"_L1) || !obj_track.contains("playcount"_L1) ||
@@ -536,14 +489,14 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
continue; continue;
} }
QJsonObject obj_artist = obj_track["artist"_L1].toObject(); const QJsonObject obj_artist = obj_track["artist"_L1].toObject();
if (!obj_artist.contains("name"_L1)) { if (!obj_artist.contains("name"_L1)) {
continue; continue;
} }
QString artist = obj_artist["name"_L1].toString(); const QString artist = obj_artist["name"_L1].toString();
QString title = obj_track["name"_L1].toString(); const QString title = obj_track["name"_L1].toString();
int playcount = obj_track["playcount"_L1].toString().toInt(); const int playcount = obj_track["playcount"_L1].toString().toInt();
if (playcount <= 0) continue; if (playcount <= 0) continue;

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2020-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2020-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,6 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList> #include <QList>
#include <QVariant> #include <QVariant>
#include <QByteArray> #include <QByteArray>
@@ -31,20 +29,28 @@
#include <QQueue> #include <QQueue>
#include <QDateTime> #include <QDateTime>
#include "core/jsonbaserequest.h"
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/jsonbaserequest.h"
class QTimer; class QTimer;
class QNetworkReply; class QNetworkReply;
class NetworkAccessManager; class NetworkAccessManager;
class LastFMImport : public QObject { class LastFMImport : public JsonBaseRequest {
Q_OBJECT Q_OBJECT
public: public:
explicit LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr); explicit LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~LastFMImport() override; ~LastFMImport() override;
QString service_name() const override { return QStringLiteral("LastFMImport"); }
bool authentication_required() const override { return false; }
bool authenticated() const override { return false; }
bool use_authorization_header() const override { return false; }
QByteArray authorization_header() const override { return QByteArray(); }
void ReloadSettings(); void ReloadSettings();
void ImportData(const bool lastplayed = true, const bool playcount = true); void ImportData(const bool lastplayed = true, const bool playcount = true);
void AbortAll(); void AbortAll();
@@ -64,8 +70,7 @@ class LastFMImport : public QObject {
private: private:
QNetworkReply *CreateRequest(const ParamList &request_params); QNetworkReply *CreateRequest(const ParamList &request_params);
QByteArray GetReplyData(QNetworkReply *reply); JsonObjectResult ParseJsonObject(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void AddGetRecentTracksRequest(const int page = 0); void AddGetRecentTracksRequest(const int page = 0);
void AddGetTopTracksRequest(const int page = 0); void AddGetTopTracksRequest(const int page = 0);
@@ -73,7 +78,7 @@ class LastFMImport : public QObject {
void SendGetRecentTracksRequest(GetRecentTracksRequest request); void SendGetRecentTracksRequest(GetRecentTracksRequest request);
void SendGetTopTracksRequest(GetTopTracksRequest request); void SendGetTopTracksRequest(GetTopTracksRequest request);
void Error(const QString &error, const QVariant &debug = QVariant()); void Error(const QString &error, const QVariant &debug = QVariant()) override;
void UpdateTotalCheck(); void UpdateTotalCheck();
void UpdateProgressCheck(); void UpdateProgressCheck();
@@ -86,7 +91,7 @@ class LastFMImport : public QObject {
void UpdateTotal(const int, const int); void UpdateTotal(const int, const int);
void UpdateProgress(const int, const int); void UpdateProgress(const int, const int);
void Finished(); void Finished();
void FinishedWithError(const QString&); void FinishedWithError(const QString &error);
private Q_SLOTS: private Q_SLOTS:
void FlushRequests(); void FlushRequests();
@@ -106,7 +111,6 @@ class LastFMImport : public QObject {
int lastplayed_received_; int lastplayed_received_;
QQueue<GetRecentTracksRequest> recent_tracks_requests_; QQueue<GetRecentTracksRequest> recent_tracks_requests_;
QQueue<GetTopTracksRequest> top_tracks_requests_; QQueue<GetTopTracksRequest> top_tracks_requests_;
QList<QNetworkReply*> replies_;
}; };
#endif // LASTFMIMPORT_H #endif // LASTFMIMPORT_H

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -19,8 +19,6 @@
#include "config.h" #include "config.h"
#include <QObject>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/networkaccessmanager.h" #include "core/networkaccessmanager.h"

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,6 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "scrobblingapi20.h" #include "scrobblingapi20.h"
@@ -40,6 +37,7 @@ class LastFMScrobbler : public ScrobblingAPI20 {
static const char *kName; static const char *kName;
static const char *kSettingsGroup; static const char *kSettingsGroup;
static const char *kApiUrl; static const char *kApiUrl;
}; };
#endif // LASTFMSCROBBLER_H #endif // LASTFMSCROBBLER_H

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -19,8 +19,6 @@
#include "config.h" #include "config.h"
#include <QObject>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/networkaccessmanager.h" #include "core/networkaccessmanager.h"

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,9 +22,6 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "scrobblingapi20.h" #include "scrobblingapi20.h"

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,18 +23,12 @@
#include <utility> #include <utility>
#include <QCoreApplication> #include <QCoreApplication>
#include <QtGlobal>
#include <QDesktopServices>
#include <QVariant> #include <QVariant>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QUrlQuery>
#include <QDateTime> #include <QDateTime>
#include <QTimer> #include <QTimer>
#include <QMessageBox>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply> #include <QNetworkReply>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
@@ -46,7 +40,7 @@
#include "core/song.h" #include "core/song.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/settings.h" #include "core/settings.h"
#include "core/localredirectserver.h" #include "core/oauthenticator.h"
#include "constants/timeconstants.h" #include "constants/timeconstants.h"
#include "constants/scrobblersettings.h" #include "constants/scrobblersettings.h"
@@ -66,6 +60,7 @@ namespace {
constexpr char kOAuthAuthorizeUrl[] = "https://musicbrainz.org/oauth2/authorize"; constexpr char kOAuthAuthorizeUrl[] = "https://musicbrainz.org/oauth2/authorize";
constexpr char kOAuthAccessTokenUrl[] = "https://musicbrainz.org/oauth2/token"; constexpr char kOAuthAccessTokenUrl[] = "https://musicbrainz.org/oauth2/token";
constexpr char kOAuthRedirectUrl[] = "http://localhost"; constexpr char kOAuthRedirectUrl[] = "http://localhost";
constexpr char kOAuthScope[] = "profile;email;tag;rating;collection;submit_isrc;submit_barcode";
constexpr char kApiUrl[] = "https://api.listenbrainz.org"; constexpr char kApiUrl[] = "https://api.listenbrainz.org";
constexpr char kClientIDB64[] = "b2VBVU53cVNRZXIwZXIwOUZpcWkwUQ=="; constexpr char kClientIDB64[] = "b2VBVU53cVNRZXIwZXIwOUZpcWkwUQ==";
constexpr char kClientSecretB64[] = "Uk9GZ2hrZVEzRjNvUHlFaHFpeVdQQQ=="; constexpr char kClientSecretB64[] = "Uk9GZ2hrZVEzRjNvUHlFaHFpeVdQQQ==";
@@ -74,44 +69,42 @@ constexpr int kScrobblesPerRequest = 10;
} // namespace } // namespace
ListenBrainzScrobbler::ListenBrainzScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent) ListenBrainzScrobbler::ListenBrainzScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent)
: ScrobblerService(QLatin1String(kName), settings, parent), : ScrobblerService(QLatin1String(kName), network, settings, parent),
network_(network), network_(network),
oauth_(new OAuthenticator(network, this)),
cache_(new ScrobblerCache(QLatin1String(kCacheFile), this)), cache_(new ScrobblerCache(QLatin1String(kCacheFile), this)),
server_(nullptr), timer_submit_(new QTimer(this)),
enabled_(false), enabled_(false),
expires_in_(-1),
login_time_(0),
submitted_(false), submitted_(false),
scrobbled_(false), scrobbled_(false),
timestamp_(0), timestamp_(0),
submit_error_(false), submit_error_(false),
prefer_albumartist_(false) { prefer_albumartist_(false) {
refresh_login_timer_.setSingleShot(true); oauth_->set_settings_group(QLatin1String(kSettingsGroup));
QObject::connect(&refresh_login_timer_, &QTimer::timeout, this, &ListenBrainzScrobbler::RequestNewAccessToken); oauth_->set_type(OAuthenticator::Type::Authorization_Code);
oauth_->set_authorize_url(QUrl(QLatin1String(kOAuthAuthorizeUrl)));
oauth_->set_redirect_url(QUrl(QLatin1String(kOAuthRedirectUrl)));
oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl)));
oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)));
oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64)));
oauth_->set_scope(QLatin1String(kOAuthScope));
oauth_->set_use_local_redirect_server(true);
oauth_->set_random_port(true);
timer_submit_.setSingleShot(true); QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &ListenBrainzScrobbler::OAuthFinished);
QObject::connect(&timer_submit_, &QTimer::timeout, this, &ListenBrainzScrobbler::Submit);
timer_submit_->setSingleShot(true);
QObject::connect(timer_submit_, &QTimer::timeout, this, &ListenBrainzScrobbler::Submit);
ListenBrainzScrobbler::ReloadSettings(); ListenBrainzScrobbler::ReloadSettings();
LoadSession(); oauth_->LoadSession();
} }
ListenBrainzScrobbler::~ListenBrainzScrobbler() { bool ListenBrainzScrobbler::authenticated() const {
while (!replies_.isEmpty()) { return !oauth_->access_token().isEmpty() && !user_token_.isEmpty();
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
if (server_) {
QObject::disconnect(server_, nullptr, this, nullptr);
if (server_->isListening()) server_->close();
server_->deleteLater();
}
} }
@@ -129,252 +122,41 @@ void ListenBrainzScrobbler::ReloadSettings() {
} }
void ListenBrainzScrobbler::LoadSession() { void ListenBrainzScrobbler::Authenticate() {
Settings s; oauth_->Authenticate();
s.beginGroup(kSettingsGroup);
access_token_ = s.value("access_token").toString();
expires_in_ = s.value("expires_in", -1).toInt();
token_type_ = s.value("token_type").toString();
refresh_token_ = s.value("refresh_token").toString();
login_time_ = s.value("login_time").toLongLong();
s.endGroup();
if (!refresh_token_.isEmpty()) { }
qint64 time = expires_in_ - (QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(login_time_));
if (time < 6) time = 6; void ListenBrainzScrobbler::Deauthenticate() {
refresh_login_timer_.setInterval(static_cast<int>(time * kMsecPerSec));
refresh_login_timer_.start(); oauth_->ClearSession();
}
} }
void ListenBrainzScrobbler::Logout() { void ListenBrainzScrobbler::Logout() {
access_token_.clear(); Deauthenticate();
token_type_.clear();
refresh_token_.clear();
expires_in_ = -1;
login_time_ = 0;
Settings s;
s.beginGroup(kSettingsGroup);
s.remove("access_token");
s.remove("expires_in");
s.remove("token_type");
s.remove("refresh_token");
s.endGroup();
} }
void ListenBrainzScrobbler::Authenticate() { void ListenBrainzScrobbler::OAuthFinished(const bool success, const QString &error) {
if (!server_) { if (success) {
server_ = new LocalRedirectServer(this); qLog(Debug) << "ListenBrainz: Authentication was successful, login expires in" << oauth_->expires_in();
if (!server_->Listen()) { Q_EMIT AuthenticationComplete(true);
AuthError(server_->error()); StartSubmit();
delete server_;
server_ = nullptr;
return;
}
QObject::connect(server_, &LocalRedirectServer::Finished, this, &ListenBrainzScrobbler::RedirectArrived);
}
QUrl redirect_url(QString::fromLatin1(kOAuthRedirectUrl));
redirect_url.setPort(server_->url().port());
QUrlQuery url_query;
url_query.addQueryItem(u"response_type"_s, u"code"_s);
url_query.addQueryItem(u"client_id"_s, QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)));
url_query.addQueryItem(u"redirect_uri"_s, redirect_url.toString());
url_query.addQueryItem(u"scope"_s, u"profile;email;tag;rating;collection;submit_isrc;submit_barcode"_s);
QUrl url(QString::fromLatin1(kOAuthAuthorizeUrl));
url.setQuery(url_query);
bool result = QDesktopServices::openUrl(url);
if (!result) {
QMessageBox messagebox(QMessageBox::Information, tr("ListenBrainz Authentication"), tr("Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
messagebox.setTextFormat(Qt::RichText);
messagebox.exec();
}
}
void ListenBrainzScrobbler::RedirectArrived() {
if (!server_) return;
if (server_->error().isEmpty()) {
QUrl url = server_->request_url();
if (url.isValid()) {
QUrlQuery url_query(url);
if (url_query.hasQueryItem(u"error"_s)) {
AuthError(QUrlQuery(url).queryItemValue(u"error"_s));
}
else if (url_query.hasQueryItem(u"code"_s)) {
RequestAccessToken(url, url_query.queryItemValue(u"code"_s));
}
else {
AuthError(tr("Redirect missing token code!"));
}
}
else {
AuthError(tr("Received invalid reply from web browser."));
}
} }
else { else {
AuthError(server_->error()); qLog(Debug) << "ListenBrainz: Authentication failed:" << error;
Q_EMIT AuthenticationComplete(false, error);
} }
server_->close();
server_->deleteLater();
server_ = nullptr;
} }
ListenBrainzScrobbler::ReplyResult ListenBrainzScrobbler::GetJsonObject(QNetworkReply *reply, QJsonObject &json_obj, QString &error_description) { QNetworkReply *ListenBrainzScrobbler::CreateRequest(const QUrl &url, const QJsonDocument &json_document) {
ReplyResult reply_error_type = ReplyResult::ServerError; return CreatePostRequest(url, json_document);
if (reply->error() == QNetworkReply::NoError) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_status_code == 200) {
reply_error_type = ReplyResult::Success;
}
else {
error_description = QStringLiteral("Received HTTP code %1").arg(http_status_code);
}
}
else {
error_description = u"Missing HTTP status code"_s;
}
}
else {
error_description = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
// See if there is Json data containing "error" and "error_description" or "code" and "error" - then use that instead.
if (reply->error() == QNetworkReply::NoError || reply->error() >= 200) {
const QByteArray data = reply->readAll();
if (!data.isEmpty() && ExtractJsonObj(data, json_obj, error_description)) {
if (json_obj.contains("error"_L1) && json_obj.contains("error_description"_L1)) {
error_description = json_obj["error_description"_L1].toString();
reply_error_type = ReplyResult::APIError;
}
else if (json_obj.contains("code"_L1) && json_obj.contains("error"_L1)) {
error_description = QStringLiteral("%1 (%2)").arg(json_obj["error"_L1].toString()).arg(json_obj["code"_L1].toInt());
reply_error_type = ReplyResult::APIError;
}
}
if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) {
// Session is probably expired
Logout();
}
}
return reply_error_type;
}
void ListenBrainzScrobbler::RequestAccessToken(const QUrl &redirect_url, const QString &code) {
refresh_login_timer_.stop();
ParamList params = ParamList() << Param(u"client_id"_s, QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)))
<< Param(u"client_secret"_s, QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64)));
if (!code.isEmpty() && !redirect_url.isEmpty()) {
params << Param(u"grant_type"_s, u"authorization_code"_s);
params << Param(u"code"_s, code);
params << Param(u"redirect_uri"_s, redirect_url.toString());
}
else if (!refresh_token_.isEmpty() && enabled_) {
params << Param(u"grant_type"_s, u"refresh_token"_s);
params << Param(u"refresh_token"_s, refresh_token_);
}
else {
return;
}
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl session_url(QString::fromLatin1(kOAuthAccessTokenUrl));
QNetworkRequest req(session_url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
replies_ << reply;
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); });
}
void ListenBrainzScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
QJsonObject json_obj;
QString error_message;
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
AuthError(error_message);
return;
}
if (!json_obj.contains("access_token"_L1) || !json_obj.contains("expires_in"_L1) || !json_obj.contains("token_type"_L1)) {
AuthError(u"Json access_token, expires_in or token_type is missing."_s);
return;
}
access_token_ = json_obj["access_token"_L1].toString();
expires_in_ = json_obj["expires_in"_L1].toInt();
token_type_ = json_obj["token_type"_L1].toString();
if (json_obj.contains("refresh_token"_L1)) {
refresh_token_ = json_obj["refresh_token"_L1].toString();
}
login_time_ = QDateTime::currentSecsSinceEpoch();
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue("access_token", access_token_);
s.setValue("expires_in", expires_in_);
s.setValue("token_type", token_type_);
s.setValue("refresh_token", refresh_token_);
s.setValue("login_time", login_time_);
s.endGroup();
if (expires_in_ > 0) {
refresh_login_timer_.setInterval(static_cast<int>(expires_in_ * kMsecPerSec));
refresh_login_timer_.start();
}
Q_EMIT AuthenticationComplete(true);
qLog(Debug) << "ListenBrainz: Authentication was successful, login expires in" << expires_in_;
StartSubmit();
}
QNetworkReply *ListenBrainzScrobbler::CreateRequest(const QUrl &url, const QJsonDocument &json_doc) {
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
req.setRawHeader("Authorization", QStringLiteral("Token %1").arg(user_token_).toUtf8());
QNetworkReply *reply = network_->post(req, json_doc.toJson());
replies_ << reply;
//qLog(Debug) << "ListenBrainz: Sending request" << json_doc.toJson();
return reply;
} }
@@ -459,6 +241,65 @@ QJsonObject ListenBrainzScrobbler::JsonTrackMetadata(const ScrobbleMetadata &met
} }
JsonBaseRequest::JsonObjectResult ListenBrainzScrobbler::ParseJsonObject(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
JsonObjectResult result(ErrorCode::Success);
result.network_error = reply->error();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("code"_L1) && json_object.contains("error"_L1)) {
const int code = json_object["code"_L1].toInt();
const QString error = json_object["error"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1 (%2)").arg(error).arg(code);
}
else if (json_object.contains("error"_L1) && json_object.contains("error_description"_L1)) {
const int error = json_object["error"_L1].toInt();
const QString error_description = json_object["error_description"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1 (%2)").arg(error_description).arg(error);
}
else {
result.json_object = json_document.object();
}
}
else {
result.error_code = ErrorCode::ParseError;
result.error_message = json_parse_error.errorString();
}
}
if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else if (result.http_status_code != 200) {
result.error_code = ErrorCode::HttpError;
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
}
}
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
oauth_->ClearSession();
}
return result;
}
void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) { void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) {
CheckScrobblePrevSong(); CheckScrobblePrevSong();
@@ -476,10 +317,9 @@ void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) {
QJsonObject object; QJsonObject object;
object.insert("listen_type"_L1, "playing_now"_L1); object.insert("listen_type"_L1, "playing_now"_L1);
object.insert("payload"_L1, array_payload); object.insert("payload"_L1, array_payload);
QJsonDocument doc(object); QJsonDocument json_document(object);
QUrl url(QStringLiteral("%1/1/submit-listens").arg(QLatin1String(kApiUrl))); QNetworkReply *reply = CreateRequest(QUrl(QStringLiteral("%1/1/submit-listens").arg(QLatin1String(kApiUrl))), json_document);
QNetworkReply *reply = CreateRequest(url, doc);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { UpdateNowPlayingRequestFinished(reply); }); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { UpdateNowPlayingRequestFinished(reply); });
} }
@@ -491,19 +331,19 @@ void ListenBrainzScrobbler::UpdateNowPlayingRequestFinished(QNetworkReply *reply
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_description; if (!json_object_result.success()) {
if (GetJsonObject(reply, json_obj, error_description) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_description);
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (!json_obj.contains("status"_L1)) { if (!json_object.contains("status"_L1)) {
Error(u"Now playing request is missing status from server."_s); Error(u"Now playing request is missing status from server."_s);
return; return;
} }
QString status = json_obj["status"_L1].toString(); const QString status = json_object["status"_L1].toString();
if (status.compare("ok"_L1, Qt::CaseInsensitive) != 0) { if (status.compare("ok"_L1, Qt::CaseInsensitive) != 0) {
Error(QStringLiteral("Received %1 status for now playing.").arg(status)); Error(QStringLiteral("Received %1 status for now playing.").arg(status));
} }
@@ -537,15 +377,15 @@ void ListenBrainzScrobbler::StartSubmit(const bool initial) {
if (!submitted_ && cache_->Count() > 0) { if (!submitted_ && cache_->Count() > 0) {
if (initial && settings_->submit_delay() <= 0 && !submit_error_) { if (initial && settings_->submit_delay() <= 0 && !submit_error_) {
if (timer_submit_.isActive()) { if (timer_submit_->isActive()) {
timer_submit_.stop(); timer_submit_->stop();
} }
Submit(); Submit();
} }
else if (!timer_submit_.isActive()) { else if (!timer_submit_->isActive()) {
int submit_delay = static_cast<int>(std::max(settings_->submit_delay(), submit_error_ ? 30 : 5) * kMsecPerSec); int submit_delay = static_cast<int>(std::max(settings_->submit_delay(), submit_error_ ? 30 : 5) * kMsecPerSec);
timer_submit_.setInterval(submit_delay); timer_submit_->setInterval(submit_delay);
timer_submit_.start(); timer_submit_->start();
} }
} }
@@ -581,7 +421,7 @@ void ListenBrainzScrobbler::Submit() {
object.insert("payload"_L1, array); object.insert("payload"_L1, array);
QJsonDocument doc(object); QJsonDocument doc(object);
QUrl url(QStringLiteral("%1/1/submit-listens").arg(QLatin1String(kApiUrl))); const QUrl url(QStringLiteral("%1/1/submit-listens").arg(QLatin1String(kApiUrl)));
QNetworkReply *reply = CreateRequest(url, doc); QNetworkReply *reply = CreateRequest(url, doc);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, cache_items_sent]() { ScrobbleRequestFinished(reply, cache_items_sent); }); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, cache_items_sent]() { ScrobbleRequestFinished(reply, cache_items_sent); });
@@ -596,12 +436,11 @@ void ListenBrainzScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, Scrobb
submitted_ = false; submitted_ = false;
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (json_object_result.success()) {
const ReplyResult reply_result = GetJsonObject(reply, json_obj, error_message); const QJsonObject &json_object = json_object_result.json_object;
if (reply_result == ReplyResult::Success) { if (json_object.contains("status"_L1)) {
if (json_obj.contains("status"_L1)) { const QString status = json_object["status"_L1].toString();
QString status = json_obj["status"_L1].toString();
qLog(Debug) << "ListenBrainz: Received scrobble status:" << status; qLog(Debug) << "ListenBrainz: Received scrobble status:" << status;
} }
else { else {
@@ -612,20 +451,20 @@ void ListenBrainzScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, Scrobb
} }
else { else {
submit_error_ = true; submit_error_ = true;
if (reply_result == ReplyResult::APIError) { if (json_object_result.error_code == ErrorCode::APIError) {
if (cache_items.count() == 1) { if (cache_items.count() == 1) {
const ScrobbleMetadata &metadata = cache_items.first()->metadata; const ScrobbleMetadata &metadata = cache_items.first()->metadata;
Error(tr("Unable to scrobble %1 - %2 because of error: %3").arg(metadata.effective_albumartist(), metadata.title, error_message)); Error(tr("Unable to scrobble %1 - %2 because of error: %3").arg(metadata.effective_albumartist(), metadata.title, json_object_result.error_message));
cache_->Flush(cache_items); cache_->Flush(cache_items);
} }
else { else {
Error(error_message); Error(json_object_result.error_message);
cache_->SetError(cache_items); cache_->SetError(cache_items);
cache_->ClearSent(cache_items); cache_->ClearSent(cache_items);
} }
} }
else { else {
Error(error_message); Error(json_object_result.error_message);
cache_->ClearSent(cache_items); cache_->ClearSent(cache_items);
} }
} }
@@ -667,33 +506,25 @@ void ListenBrainzScrobbler::LoveRequestFinished(QNetworkReply *reply) {
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (!json_object_result.success()) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_message);
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (json_obj.contains("status"_L1)) { if (json_object.contains("status"_L1)) {
qLog(Debug) << "ListenBrainz: Received recording-feedback status:" << json_obj["status"_L1].toString(); qLog(Debug) << "ListenBrainz: Received recording-feedback status:" << json_object["status"_L1].toString();
} }
} }
void ListenBrainzScrobbler::AuthError(const QString &error) { void ListenBrainzScrobbler::Error(const QString &error_message, const QVariant &debug_output) {
qLog(Error) << "ListenBrainz" << error; JsonBaseRequest::Error(error_message, debug_output);
Q_EMIT AuthenticationComplete(false, error);
}
void ListenBrainzScrobbler::Error(const QString &error, const QVariant &debug) {
qLog(Error) << "ListenBrainz:" << error;
if (debug.isValid()) qLog(Debug) << debug;
if (settings_->show_error_dialog()) { if (settings_->show_error_dialog()) {
Q_EMIT ErrorMessage(tr("ListenBrainz error: %1").arg(error)); Q_EMIT ErrorMessage(tr("ListenBrainz error: %1").arg(error_message));
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,15 +22,11 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QVariant> #include <QVariant>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QJsonDocument> #include <QJsonDocument>
#include <QTimer>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/song.h" #include "core/song.h"
@@ -38,31 +34,34 @@
#include "scrobblercache.h" #include "scrobblercache.h"
#include "scrobblemetadata.h" #include "scrobblemetadata.h"
class QTimer;
class QNetworkReply; class QNetworkReply;
class ScrobblerSettingsService; class ScrobblerSettingsService;
class NetworkAccessManager; class NetworkAccessManager;
class LocalRedirectServer; class OAuthenticator;
class ListenBrainzScrobbler : public ScrobblerService { class ListenBrainzScrobbler : public ScrobblerService {
Q_OBJECT Q_OBJECT
public: public:
explicit ListenBrainzScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr); explicit ListenBrainzScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~ListenBrainzScrobbler() override;
static const char *kName; static const char *kName;
static const char *kSettingsGroup; static const char *kSettingsGroup;
void ReloadSettings() override; void ReloadSettings() override;
void LoadSession();
bool enabled() const override { return enabled_; } bool enabled() const override { return enabled_; }
bool authenticated() const override { return !access_token_.isEmpty() && !user_token_.isEmpty(); } bool authentication_required() const override { return true; }
bool authenticated() const override;
bool use_authorization_header() const override { return true; }
QByteArray authorization_header() const override { return "Token " + user_token_.toUtf8(); }
bool submitted() const override { return submitted_; } bool submitted() const override { return submitted_; }
QString user_token() const { return user_token_; } QString user_token() const { return user_token_; }
void Authenticate(); void Authenticate();
void Deauthenticate();
void Logout(); void Logout();
void Submit() override; void Submit() override;
void UpdateNowPlaying(const Song &song) override; void UpdateNowPlaying(const Song &song) override;
@@ -70,12 +69,6 @@ class ListenBrainzScrobbler : public ScrobblerService {
void Scrobble(const Song &song) override; void Scrobble(const Song &song) override;
void Love() override; void Love() override;
enum class ReplyResult {
Success,
ServerError,
APIError
};
Q_SIGNALS: Q_SIGNALS:
void AuthenticationComplete(const bool success, const QString &error = QString()); void AuthenticationComplete(const bool success, const QString &error = QString());
@@ -83,44 +76,32 @@ class ListenBrainzScrobbler : public ScrobblerService {
void WriteCache() override { cache_->WriteCache(); } void WriteCache() override { cache_->WriteCache(); }
private Q_SLOTS: private Q_SLOTS:
void RedirectArrived(); void OAuthFinished(const bool success, const QString &error);
void AuthenticateReplyFinished(QNetworkReply *reply);
void RequestNewAccessToken() { RequestAccessToken(); }
void UpdateNowPlayingRequestFinished(QNetworkReply *reply); void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
void ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items); void ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items);
void LoveRequestFinished(QNetworkReply *reply); void LoveRequestFinished(QNetworkReply *reply);
private: private:
QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_doc); QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_document);
ReplyResult GetJsonObject(QNetworkReply *reply, QJsonObject &json_obj, QString &error_description);
QJsonObject JsonTrackMetadata(const ScrobbleMetadata &metadata) const; QJsonObject JsonTrackMetadata(const ScrobbleMetadata &metadata) const;
void AuthError(const QString &error); JsonObjectResult ParseJsonObject(QNetworkReply *reply);
void Error(const QString &error, const QVariant &debug = QVariant()); void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
void RequestAccessToken(const QUrl &redirect_url = QUrl(), const QString &code = QString());
void StartSubmit(const bool initial = false) override; void StartSubmit(const bool initial = false) override;
void CheckScrobblePrevSong(); void CheckScrobblePrevSong();
const SharedPtr<NetworkAccessManager> network_; const SharedPtr<NetworkAccessManager> network_;
OAuthenticator *oauth_;
ScrobblerCache *cache_; ScrobblerCache *cache_;
LocalRedirectServer *server_; QTimer *timer_submit_;
bool enabled_; bool enabled_;
QString user_token_; QString user_token_;
QString access_token_;
qint64 expires_in_;
QString token_type_;
QString refresh_token_;
quint64 login_time_;
bool submitted_; bool submitted_;
Song song_playing_; Song song_playing_;
bool scrobbled_; bool scrobbled_;
quint64 timestamp_; quint64 timestamp_;
QTimer refresh_login_timer_;
QTimer timer_submit_;
bool submit_error_; bool submit_error_;
bool prefer_albumartist_; bool prefer_albumartist_;
QList<QNetworkReply*> replies_;
}; };
#endif // LISTENBRAINZSCROBBLER_H #endif // LISTENBRAINZSCROBBLER_H

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -19,37 +19,14 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QString> #include <QString>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "scrobblerservice.h" #include "scrobblerservice.h"
#include "scrobblersettingsservice.h" #include "scrobblersettingsservice.h"
#include "core/song.h" #include "core/song.h"
ScrobblerService::ScrobblerService(const QString &name, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent) : QObject(parent), name_(name), settings_(settings) {} ScrobblerService::ScrobblerService(const QString &name, const SharedPtr<NetworkAccessManager> network, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent) : JsonBaseRequest(network, parent), name_(name), settings_(settings) {}
bool ScrobblerService::ExtractJsonObj(const QByteArray &data, QJsonObject &json_obj, QString &error_description) {
QJsonParseError json_parse_error;
const QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error != QJsonParseError::NoError) {
error_description = json_parse_error.errorString();
return false;
}
if (json_doc.isObject()) {
json_obj = json_doc.object();
}
return true;
}
QString ScrobblerService::StripAlbum(const QString &album) const { QString ScrobblerService::StripAlbum(const QString &album) const {

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,31 +22,29 @@
#include "config.h" #include "config.h"
#include <QObject>
#include <QPair> #include <QPair>
#include <QList>
#include <QVariant>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QJsonObject>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/song.h" #include "core/song.h"
#include "core/jsonbaserequest.h"
#include "scrobblersettingsservice.h" #include "scrobblersettingsservice.h"
class ScrobblerService : public QObject { class ScrobblerService : public JsonBaseRequest {
Q_OBJECT Q_OBJECT
public: public:
explicit ScrobblerService(const QString &name, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent); explicit ScrobblerService(const QString &name, const SharedPtr<NetworkAccessManager> network, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent);
QString name() const { return name_; } QString name() const { return name_; }
QString service_name() const override { return name_; }
virtual void ReloadSettings() = 0; virtual void ReloadSettings() = 0;
virtual bool enabled() const { return false; } virtual bool enabled() const { return false; }
virtual bool authenticated() const { return false; } virtual bool authenticated() const override { return false; }
virtual void UpdateNowPlaying(const Song &song) = 0; virtual void UpdateNowPlaying(const Song &song) = 0;
virtual void ClearPlaying() = 0; virtual void ClearPlaying() = 0;
@@ -57,12 +55,8 @@ class ScrobblerService : public QObject {
virtual bool submitted() const { return false; } virtual bool submitted() const { return false; }
protected: protected:
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
using EncodedParam = QPair<QByteArray, QByteArray>; using EncodedParam = QPair<QByteArray, QByteArray>;
bool ExtractJsonObj(const QByteArray &data, QJsonObject &json_obj, QString &error_description);
QString StripAlbum(const QString &album) const; QString StripAlbum(const QString &album) const;
QString StripTitle(const QString &title) const; QString StripTitle(const QString &title) const;

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -68,10 +68,10 @@ const char *ScrobblingAPI20::kApiKey = "211990b4c96782c05d1536e7219eb56e";
namespace { namespace {
constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8"; constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8";
constexpr int kScrobblesPerRequest = 50; constexpr int kScrobblesPerRequest = 50;
} } // namespace
ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, const QString &cache_file, const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent) ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, const QString &cache_file, const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent)
: ScrobblerService(name, settings, parent), : ScrobblerService(name, network, settings, parent),
name_(name), name_(name),
settings_group_(settings_group), settings_group_(settings_group),
auth_url_(auth_url), auth_url_(auth_url),
@@ -79,17 +79,18 @@ ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_gr
batch_(batch), batch_(batch),
network_(network), network_(network),
cache_(new ScrobblerCache(cache_file, this)), cache_(new ScrobblerCache(cache_file, this)),
server_(nullptr), local_redirect_server_(nullptr),
enabled_(false), enabled_(false),
prefer_albumartist_(false), prefer_albumartist_(false),
subscriber_(false), subscriber_(false),
submitted_(false), submitted_(false),
scrobbled_(false), scrobbled_(false),
timestamp_(0), timestamp_(0),
submit_error_(false) { submit_error_(false),
timer_submit_(new QTimer(this)) {
timer_submit_.setSingleShot(true); timer_submit_->setSingleShot(true);
QObject::connect(&timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit); QObject::connect(timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit);
ScrobblingAPI20::ReloadSettings(); ScrobblingAPI20::ReloadSettings();
LoadSession(); LoadSession();
@@ -98,17 +99,10 @@ ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_gr
ScrobblingAPI20::~ScrobblingAPI20() { ScrobblingAPI20::~ScrobblingAPI20() {
while (!replies_.isEmpty()) { if (local_redirect_server_) {
QNetworkReply *reply = replies_.takeFirst(); QObject::disconnect(local_redirect_server_, nullptr, this, nullptr);
QObject::disconnect(reply, nullptr, this, nullptr); if (local_redirect_server_->isListening()) local_redirect_server_->close();
reply->abort(); local_redirect_server_->deleteLater();
reply->deleteLater();
}
if (server_) {
QObject::disconnect(server_, nullptr, this, nullptr);
if (server_->isListening()) server_->close();
server_->deleteLater();
} }
} }
@@ -138,7 +132,7 @@ void ScrobblingAPI20::LoadSession() {
} }
void ScrobblingAPI20::Logout() { void ScrobblingAPI20::ClearSession() {
subscriber_ = false; subscriber_ = false;
username_.clear(); username_.clear();
@@ -153,70 +147,22 @@ void ScrobblingAPI20::Logout() {
} }
ScrobblingAPI20::ReplyResult ScrobblingAPI20::GetJsonObject(QNetworkReply *reply, QJsonObject &json_obj, QString &error_description) {
ReplyResult reply_error_type = ReplyResult::ServerError;
if (reply->error() == QNetworkReply::NoError) {
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_status_code == 200) {
reply_error_type = ReplyResult::Success;
}
else {
error_description = QStringLiteral("Received HTTP code %1").arg(http_status_code);
}
}
else {
error_description = u"Missing HTTP status code"_s;
}
}
else {
error_description = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
// See if there is Json data containing "error" and "message" - then use that instead.
if (reply->error() == QNetworkReply::NoError || reply->error() >= 200) {
const QByteArray data = reply->readAll();
int error_code = 0;
if (!data.isEmpty() && ExtractJsonObj(data, json_obj, error_description) && json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) {
error_code = json_obj["error"_L1].toInt();
QString error_message = json_obj["message"_L1].toString();
error_description = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
reply_error_type = ReplyResult::APIError;
}
const ScrobbleErrorCode lastfm_error_code = static_cast<ScrobbleErrorCode>(error_code);
if (reply->error() == QNetworkReply::AuthenticationRequiredError ||
lastfm_error_code == ScrobbleErrorCode::InvalidSessionKey ||
lastfm_error_code == ScrobbleErrorCode::UnauthorizedToken ||
lastfm_error_code == ScrobbleErrorCode::LoginRequired ||
lastfm_error_code == ScrobbleErrorCode::APIKeySuspended
) {
// Session is probably expired
Logout();
}
}
return reply_error_type;
}
void ScrobblingAPI20::Authenticate() { void ScrobblingAPI20::Authenticate() {
if (!server_) { if (!local_redirect_server_) {
server_ = new LocalRedirectServer(this); local_redirect_server_ = new LocalRedirectServer(this);
if (!server_->Listen()) { if (!local_redirect_server_->Listen()) {
AuthError(server_->error()); AuthError(local_redirect_server_->error());
delete server_; delete local_redirect_server_;
server_ = nullptr; local_redirect_server_ = nullptr;
return; return;
} }
QObject::connect(server_, &LocalRedirectServer::Finished, this, &ScrobblingAPI20::RedirectArrived); QObject::connect(local_redirect_server_, &LocalRedirectServer::Finished, this, &ScrobblingAPI20::RedirectArrived);
} }
QUrlQuery url_query; QUrlQuery url_query;
url_query.addQueryItem(u"api_key"_s, QLatin1String(kApiKey)); url_query.addQueryItem(u"api_key"_s, QLatin1String(kApiKey));
url_query.addQueryItem(u"cb"_s, server_->url().toString()); url_query.addQueryItem(u"cb"_s, local_redirect_server_->url().toString());
QUrl url(auth_url_); QUrl url(auth_url_);
url.setQuery(url_query); url.setQuery(url_query);
@@ -238,10 +184,10 @@ void ScrobblingAPI20::Authenticate() {
QApplication::clipboard()->setText(url.toString()); QApplication::clipboard()->setText(url.toString());
break; break;
case QMessageBox::Cancel: case QMessageBox::Cancel:
if (server_) { if (local_redirect_server_) {
server_->close(); local_redirect_server_->close();
server_->deleteLater(); local_redirect_server_->deleteLater();
server_ = nullptr; local_redirect_server_ = nullptr;
} }
Q_EMIT AuthenticationComplete(false); Q_EMIT AuthenticationComplete(false);
break; break;
@@ -253,10 +199,10 @@ void ScrobblingAPI20::Authenticate() {
void ScrobblingAPI20::RedirectArrived() { void ScrobblingAPI20::RedirectArrived() {
if (!server_) return; if (!local_redirect_server_) return;
if (server_->error().isEmpty()) { if (local_redirect_server_->error().isEmpty()) {
QUrl url = server_->request_url(); const QUrl url = local_redirect_server_->request_url();
if (url.isValid()) { if (url.isValid()) {
QUrlQuery url_query(url); QUrlQuery url_query(url);
if (url_query.hasQueryItem(u"token"_s)) { if (url_query.hasQueryItem(u"token"_s)) {
@@ -272,12 +218,12 @@ void ScrobblingAPI20::RedirectArrived() {
} }
} }
else { else {
AuthError(server_->error()); AuthError(local_redirect_server_->error());
} }
server_->close(); local_redirect_server_->close();
server_->deleteLater(); local_redirect_server_->deleteLater();
server_ = nullptr; local_redirect_server_ = nullptr;
} }
@@ -300,10 +246,7 @@ void ScrobblingAPI20::RequestSession(const QString &token) {
session_url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s))); session_url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s)));
session_url.setQuery(session_url_query); session_url.setQuery(session_url_query);
QNetworkRequest req(session_url); QNetworkReply *reply = CreateGetRequest(session_url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(req);
replies_ << reply;
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); }); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); });
} }
@@ -315,36 +258,36 @@ void ScrobblingAPI20::AuthenticateReplyFinished(QNetworkReply *reply) {
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (json_object_result.error_code != ErrorCode::Success) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { AuthError(json_object_result.error_message);
AuthError(error_message);
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (!json_obj.contains("session"_L1)) { if (!json_object.contains("session"_L1)) {
AuthError(u"Json reply from server is missing session."_s); AuthError(u"Json reply from server is missing session."_s);
return; return;
} }
QJsonValue json_session = json_obj["session"_L1]; const QJsonValue json_value_session = json_object["session"_L1];
if (!json_session.isObject()) { if (!json_value_session.isObject()) {
AuthError(u"Json session is not an object."_s); AuthError(u"Json session is not an object."_s);
return; return;
} }
json_obj = json_session.toObject(); const QJsonObject json_object_session = json_value_session.toObject();
if (json_obj.isEmpty()) { if (json_object_session.isEmpty()) {
AuthError(u"Json session object is empty."_s); AuthError(u"Json session object is empty."_s);
return; return;
} }
if (!json_obj.contains("subscriber"_L1) || !json_obj.contains("name"_L1) || !json_obj.contains("key"_L1)) { if (!json_object_session.contains("subscriber"_L1) || !json_object_session.contains("name"_L1) || !json_object_session.contains("key"_L1)) {
AuthError(u"Json session object is missing values."_s); AuthError(u"Json session object is missing values."_s);
return; return;
} }
subscriber_ = json_obj["subscriber"_L1].toBool(); subscriber_ = json_object_session["subscriber"_L1].toBool();
username_ = json_obj["name"_L1].toString(); username_ = json_object_session["name"_L1].toString();
session_key_ = json_obj["key"_L1].toString(); session_key_ = json_object_session["key"_L1].toString();
Settings s; Settings s;
s.beginGroup(settings_group_); s.beginGroup(settings_group_);
@@ -384,17 +327,60 @@ QNetworkReply *ScrobblingAPI20::CreateRequest(const ParamList &request_params) {
url_query.addQueryItem(u"api_sig"_s, QString::fromLatin1(QUrl::toPercentEncoding(signature))); url_query.addQueryItem(u"api_sig"_s, QString::fromLatin1(QUrl::toPercentEncoding(signature)));
url_query.addQueryItem(u"format"_s, u"json"_s); url_query.addQueryItem(u"format"_s, u"json"_s);
QUrl url(api_url_); return CreatePostRequest(QUrl(api_url_), url_query);
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8();
QNetworkReply *reply = network_->post(req, query);
replies_ << reply;
//qLog(Debug) << name_ << "Sending request" << url_query.toString(QUrl::FullyDecoded); }
return reply; JsonBaseRequest::JsonObjectResult ScrobblingAPI20::ParseJsonObject(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
JsonObjectResult result(ErrorCode::Success);
result.network_error = reply->error();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("error"_L1) && json_object.contains("message"_L1)) {
const int error = json_object["error"_L1].toInt();
const QString message = json_object["message"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(error);
}
else {
result.json_object = json_document.object();
}
}
else {
result.error_code = ErrorCode::ParseError;
result.error_message = json_parse_error.errorString();
}
}
if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else if (result.http_status_code != 200) {
result.error_code = ErrorCode::HttpError;
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
}
}
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
ClearSession();
}
return result;
} }
@@ -433,15 +419,15 @@ void ScrobblingAPI20::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (json_object_result.error_code != ErrorCode::Success) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_message);
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (!json_obj.contains("nowplaying"_L1)) { if (!json_object.contains("nowplaying"_L1)) {
Error(u"Json reply from server is missing nowplaying."_s, json_obj); Error(u"Json reply from server is missing nowplaying."_s, json_object);
return; return;
} }
@@ -482,15 +468,15 @@ void ScrobblingAPI20::StartSubmit(const bool initial) {
if (!submitted_ && cache_->Count() > 0) { if (!submitted_ && cache_->Count() > 0) {
if (initial && (!batch_ || settings_->submit_delay() <= 0) && !submit_error_) { if (initial && (!batch_ || settings_->submit_delay() <= 0) && !submit_error_) {
if (timer_submit_.isActive()) { if (timer_submit_->isActive()) {
timer_submit_.stop(); timer_submit_->stop();
} }
Submit(); Submit();
} }
else if (!timer_submit_.isActive()) { else if (!timer_submit_->isActive()) {
int submit_delay = static_cast<int>(std::max(settings_->submit_delay(), submit_error_ ? 30 : 5) * kMsecPerSec); int submit_delay = static_cast<int>(std::max(settings_->submit_delay(), submit_error_ ? 30 : 5) * kMsecPerSec);
timer_submit_.setInterval(submit_delay); timer_submit_->setInterval(submit_delay);
timer_submit_.start(); timer_submit_->start();
} }
} }
@@ -550,68 +536,68 @@ void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCac
submitted_ = false; submitted_ = false;
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (!json_object_result.success()) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_message);
cache_->ClearSent(cache_items); cache_->ClearSent(cache_items);
submit_error_ = true; submit_error_ = true;
StartSubmit(); StartSubmit();
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
cache_->Flush(cache_items); cache_->Flush(cache_items);
submit_error_ = false; submit_error_ = false;
if (!json_obj.contains("scrobbles"_L1)) { if (!json_object.contains("scrobbles"_L1)) {
Error(u"Json reply from server is missing scrobbles."_s, json_obj); Error(u"Json reply from server is missing scrobbles."_s, json_object);
StartSubmit(); StartSubmit();
return; return;
} }
QJsonValue value_scrobbles = json_obj["scrobbles"_L1]; const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
if (!value_scrobbles.isObject()) { if (!value_scrobbles.isObject()) {
Error(u"Json scrobbles is not an object."_s, json_obj); Error(u"Json scrobbles is not an object."_s, json_object);
StartSubmit(); StartSubmit();
return; return;
} }
json_obj = value_scrobbles.toObject(); const QJsonObject object_scrobbles = value_scrobbles.toObject();
if (json_obj.isEmpty()) { if (object_scrobbles.isEmpty()) {
Error(u"Json scrobbles object is empty."_s, value_scrobbles); Error(u"Json scrobbles object is empty."_s, value_scrobbles);
StartSubmit(); StartSubmit();
return; return;
} }
if (!json_obj.contains("@attr"_L1) || !json_obj.contains("scrobble"_L1)) { if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
Error(u"Json scrobbles object is missing values."_s, json_obj); Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
StartSubmit(); StartSubmit();
return; return;
} }
QJsonValue value_attr = json_obj["@attr"_L1]; const QJsonValue value_attr = object_scrobbles["@attr"_L1];
if (!value_attr.isObject()) { if (!value_attr.isObject()) {
Error(u"Json scrobbles attr is not an object."_s, value_attr); Error(u"Json scrobbles attr is not an object."_s, value_attr);
StartSubmit(); StartSubmit();
return; return;
} }
QJsonObject obj_attr = value_attr.toObject(); const QJsonObject object_attr = value_attr.toObject();
if (obj_attr.isEmpty()) { if (object_attr.isEmpty()) {
Error(u"Json scrobbles attr is empty."_s, value_attr); Error(u"Json scrobbles attr is empty."_s, value_attr);
StartSubmit(); StartSubmit();
return; return;
} }
if (!obj_attr.contains("accepted"_L1) || !obj_attr.contains("ignored"_L1)) { if (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
Error(u"Json scrobbles attr is missing values."_s, obj_attr); Error(u"Json scrobbles attr is missing values."_s, object_attr);
StartSubmit(); StartSubmit();
return; return;
} }
int accepted = obj_attr["accepted"_L1].toInt(); int accepted = object_attr["accepted"_L1].toInt();
int ignored = obj_attr["ignored"_L1].toInt(); int ignored = object_attr["ignored"_L1].toInt();
qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored; qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
QJsonArray array_scrobble; QJsonArray array_scrobble;
QJsonValue value_scrobble = json_obj["scrobble"_L1]; const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
if (value_scrobble.isObject()) { if (value_scrobble.isObject()) {
QJsonObject obj_scrobble = value_scrobble.toObject(); QJsonObject obj_scrobble = value_scrobble.toObject();
if (obj_scrobble.isEmpty()) { if (obj_scrobble.isEmpty()) {
@@ -657,36 +643,36 @@ void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCac
continue; continue;
} }
QJsonValue value_artist = json_track["artist"_L1]; const QJsonValue value_artist = json_track["artist"_L1];
QJsonValue value_album = json_track["album"_L1]; const QJsonValue value_album = json_track["album"_L1];
QJsonValue value_song = json_track["track"_L1]; const QJsonValue value_song = json_track["track"_L1];
QJsonValue value_ignoredmessage = json_track["ignoredMessage"_L1]; const QJsonValue value_ignoredmessage = json_track["ignoredMessage"_L1];
//quint64 timestamp = json_track[u"timestamp"_s].toVariant().toULongLong(); //const quint64 timestamp = json_track[u"timestamp"_s].toVariant().toULongLong();
if (!value_artist.isObject() || !value_album.isObject() || !value_song.isObject() || !value_ignoredmessage.isObject()) { if (!value_artist.isObject() || !value_album.isObject() || !value_song.isObject() || !value_ignoredmessage.isObject()) {
Error(u"Json scrobbles scrobble values are not objects."_s, json_track); Error(u"Json scrobbles scrobble values are not objects."_s, json_track);
continue; continue;
} }
QJsonObject obj_artist = value_artist.toObject(); const QJsonObject object_artist = value_artist.toObject();
QJsonObject obj_album = value_album.toObject(); const QJsonObject object_album = value_album.toObject();
QJsonObject obj_song = value_song.toObject(); const QJsonObject object_song = value_song.toObject();
QJsonObject obj_ignoredmessage = value_ignoredmessage.toObject(); const QJsonObject object_ignoredmessage = value_ignoredmessage.toObject();
if (obj_artist.isEmpty() || obj_album.isEmpty() || obj_song.isEmpty() || obj_ignoredmessage.isEmpty()) { if (object_artist.isEmpty() || object_album.isEmpty() || object_song.isEmpty() || object_ignoredmessage.isEmpty()) {
Error(u"Json scrobbles scrobble values objects are empty."_s, json_track); Error(u"Json scrobbles scrobble values objects are empty."_s, json_track);
continue; continue;
} }
if (!obj_artist.contains("#text"_L1) || !obj_album.contains("#text"_L1) || !obj_song.contains("#text"_L1)) { if (!object_artist.contains("#text"_L1) || !object_album.contains("#text"_L1) || !object_song.contains("#text"_L1)) {
continue; continue;
} }
//QString artist = obj_artist["#text"].toString(); //const QString artist = obj_artist["#text"].toString();
//QString album = obj_album["#text"].toString(); //const QString album = obj_album["#text"].toString();
QString song = obj_song["#text"_L1].toString(); const QString song = object_song["#text"_L1].toString();
bool ignoredmessage = obj_ignoredmessage["code"_L1].toVariant().toBool(); const bool ignoredmessage = object_ignoredmessage["code"_L1].toVariant().toBool();
QString ignoredmessage_text = obj_ignoredmessage["#text"_L1].toString(); const QString ignoredmessage_text = object_ignoredmessage["#text"_L1].toString();
if (ignoredmessage) { if (ignoredmessage) {
Error(u"Scrobble for \"%1\" ignored: %2"_s.arg(song, ignoredmessage_text)); Error(u"Scrobble for \"%1\" ignored: %2"_s.arg(song, ignoredmessage_text));
@@ -732,97 +718,97 @@ void ScrobblingAPI20::SingleScrobbleRequestFinished(QNetworkReply *reply, Scrobb
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (json_object_result.error_code != ErrorCode::Success) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_message);
cache_item->sent = false; cache_item->sent = false;
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (!json_obj.contains("scrobbles"_L1)) { if (!json_object.contains("scrobbles"_L1)) {
Error(u"Json reply from server is missing scrobbles."_s, json_obj); Error(u"Json reply from server is missing scrobbles."_s, json_object);
cache_item->sent = false; cache_item->sent = false;
return; return;
} }
cache_->Remove(cache_item); cache_->Remove(cache_item);
QJsonValue value_scrobbles = json_obj["scrobbles"_L1]; const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
if (!value_scrobbles.isObject()) { if (!value_scrobbles.isObject()) {
Error(u"Json scrobbles is not an object."_s, json_obj); Error(u"Json scrobbles is not an object."_s, json_object);
return; return;
} }
json_obj = value_scrobbles.toObject(); const QJsonObject object_scrobbles = value_scrobbles.toObject();
if (json_obj.isEmpty()) { if (object_scrobbles.isEmpty()) {
Error(u"Json scrobbles object is empty."_s, value_scrobbles); Error(u"Json scrobbles object is empty."_s, value_scrobbles);
return; return;
} }
if (!json_obj.contains("@attr"_L1) || !json_obj.contains("scrobble"_L1)) { if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
Error(u"Json scrobbles object is missing values."_s, json_obj); Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
return; return;
} }
QJsonValue value_attr = json_obj["@attr"_L1]; const QJsonValue value_attr = object_scrobbles["@attr"_L1];
if (!value_attr.isObject()) { if (!value_attr.isObject()) {
Error(u"Json scrobbles attr is not an object."_s, value_attr); Error(u"Json scrobbles attr is not an object."_s, value_attr);
return; return;
} }
QJsonObject obj_attr = value_attr.toObject(); const QJsonObject object_attr = value_attr.toObject();
if (obj_attr.isEmpty()) { if (object_attr.isEmpty()) {
Error(u"Json scrobbles attr is empty."_s, value_attr); Error(u"Json scrobbles attr is empty."_s, value_attr);
return; return;
} }
QJsonValue value_scrobble = json_obj["scrobble"_L1]; const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
if (!value_scrobble.isObject()) { if (!value_scrobble.isObject()) {
Error(u"Json scrobbles scrobble is not an object."_s, value_scrobble); Error(u"Json scrobbles scrobble is not an object."_s, value_scrobble);
return; return;
} }
QJsonObject json_obj_scrobble = value_scrobble.toObject(); const QJsonObject json_object_scrobble = value_scrobble.toObject();
if (json_obj_scrobble.isEmpty()) { if (json_object_scrobble.isEmpty()) {
Error(u"Json scrobbles scrobble is empty."_s, value_scrobble); Error(u"Json scrobbles scrobble is empty."_s, value_scrobble);
return; return;
} }
if (!obj_attr.contains("accepted"_L1) || !obj_attr.contains("ignored"_L1)) { if (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
Error(u"Json scrobbles attr is missing values."_s, obj_attr); Error(u"Json scrobbles attr is missing values."_s, object_attr);
return; return;
} }
if (!json_obj_scrobble.contains("artist"_L1) || !json_obj_scrobble.contains("album"_L1) || !json_obj_scrobble.contains("albumArtist"_L1) || !json_obj_scrobble.contains("track"_L1) || !json_obj_scrobble.contains("timestamp"_L1)) { if (!json_object_scrobble.contains("artist"_L1) || !json_object_scrobble.contains("album"_L1) || !json_object_scrobble.contains("albumArtist"_L1) || !json_object_scrobble.contains("track"_L1) || !json_object_scrobble.contains("timestamp"_L1)) {
Error(u"Json scrobbles scrobble is missing values."_s, json_obj_scrobble); Error(u"Json scrobbles scrobble is missing values."_s, json_object_scrobble);
return; return;
} }
QJsonValue json_value_artist = json_obj_scrobble["artist"_L1]; const QJsonValue json_value_artist = json_object_scrobble["artist"_L1];
QJsonValue json_value_album = json_obj_scrobble["album"_L1]; const QJsonValue json_value_album = json_object_scrobble["album"_L1];
QJsonValue json_value_song = json_obj_scrobble["track"_L1]; const QJsonValue json_value_song = json_object_scrobble["track"_L1];
if (!json_value_artist.isObject() || !json_value_album.isObject() || !json_value_song.isObject()) { if (!json_value_artist.isObject() || !json_value_album.isObject() || !json_value_song.isObject()) {
Error(u"Json scrobbles scrobble values are not objects."_s, json_obj_scrobble); Error(u"Json scrobbles scrobble values are not objects."_s, json_object_scrobble);
return; return;
} }
QJsonObject json_obj_artist = json_value_artist.toObject(); const QJsonObject json_object_artist = json_value_artist.toObject();
QJsonObject json_obj_album = json_value_album.toObject(); const QJsonObject json_object_album = json_value_album.toObject();
QJsonObject json_obj_song = json_value_song.toObject(); const QJsonObject json_object_song = json_value_song.toObject();
if (json_obj_artist.isEmpty() || json_obj_album.isEmpty() || json_obj_song.isEmpty()) { if (json_object_artist.isEmpty() || json_object_album.isEmpty() || json_object_song.isEmpty()) {
Error(u"Json scrobbles scrobble values objects are empty."_s, json_obj_scrobble); Error(u"Json scrobbles scrobble values objects are empty."_s, json_object_scrobble);
return; return;
} }
if (!json_obj_artist.contains("#text"_L1) || !json_obj_album.contains("#text"_L1) || !json_obj_song.contains("#text"_L1)) { if (!json_object_artist.contains("#text"_L1) || !json_object_album.contains("#text"_L1) || !json_object_song.contains("#text"_L1)) {
Error(u"Json scrobbles scrobble values objects are missing #text."_s, json_obj_artist); Error(u"Json scrobbles scrobble values objects are missing #text."_s, json_object_artist);
return; return;
} }
//QString artist = json_obj_artist["#text"].toString(); //QString artist = json_obj_artist["#text"].toString();
//QString album = json_obj_album["#text"].toString(); //QString album = json_obj_album["#text"].toString();
QString song = json_obj_song["#text"_L1].toString(); const QString song = json_object_song["#text"_L1].toString();
int accepted = obj_attr["accepted"_L1].toVariant().toInt(); const int accepted = object_attr["accepted"_L1].toVariant().toInt();
if (accepted == 1) { if (accepted == 1) {
qLog(Debug) << name_ << "Scrobble for" << song << "accepted"; qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
} }
@@ -868,22 +854,22 @@ void ScrobblingAPI20::LoveRequestFinished(QNetworkReply *reply) {
QObject::disconnect(reply, nullptr, this, nullptr); QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater(); reply->deleteLater();
QJsonObject json_obj; const JsonObjectResult json_object_result = ParseJsonObject(reply);
QString error_message; if (json_object_result.error_code != ErrorCode::Success) {
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) { Error(json_object_result.error_message);
Error(error_message);
return; return;
} }
const QJsonObject &json_object = json_object_result.json_object;
if (json_obj.contains("error"_L1)) { if (json_object.contains("error"_L1)) {
QJsonValue json_value = json_obj["error"_L1]; const QJsonValue json_value = json_object["error"_L1];
if (!json_value.isObject()) { if (!json_value.isObject()) {
Error(u"Error is not on object."_s); Error(u"Error is not on object."_s);
return; return;
} }
QJsonObject json_obj_error = json_value.toObject(); const QJsonObject json_obj_error = json_value.toObject();
if (json_obj_error.isEmpty()) { if (json_obj_error.isEmpty()) {
Error(u"Received empty json error object."_s, json_obj); Error(u"Received empty json error object."_s, json_object);
return; return;
} }
if (json_obj_error.contains("code"_L1) && json_obj_error.contains("#text"_L1)) { if (json_obj_error.contains("code"_L1) && json_obj_error.contains("#text"_L1)) {
@@ -895,12 +881,12 @@ void ScrobblingAPI20::LoveRequestFinished(QNetworkReply *reply) {
} }
} }
if (json_obj.contains("lfm"_L1)) { if (json_object.contains("lfm"_L1)) {
QJsonValue json_value = json_obj["lfm"_L1]; const QJsonValue json_value = json_object["lfm"_L1];
if (json_value.isObject()) { if (json_value.isObject()) {
QJsonObject json_obj_lfm = json_value.toObject(); const QJsonObject json_obj_lfm = json_value.toObject();
if (json_obj_lfm.contains("status"_L1)) { if (json_obj_lfm.contains("status"_L1)) {
QString status = json_obj_lfm["status"_L1].toString(); const QString status = json_obj_lfm["status"_L1].toString();
qLog(Debug) << name_ << "Received love status:" << status; qLog(Debug) << name_ << "Received love status:" << status;
return; return;
} }
@@ -918,8 +904,7 @@ void ScrobblingAPI20::AuthError(const QString &error) {
void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) { void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) {
qLog(Error) << name_ << error; JsonBaseRequest::Error(error, debug);
if (debug.isValid()) qLog(Debug) << debug;
if (settings_->show_error_dialog()) { if (settings_->show_error_dialog()) {
Q_EMIT ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error)); Q_EMIT ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error));
@@ -994,8 +979,7 @@ QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) {
void ScrobblingAPI20::CheckScrobblePrevSong() { void ScrobblingAPI20::CheckScrobblePrevSong() {
qint64 duration = QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_); const qint64 duration = std::min(0LL, QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_));
if (duration < 0) duration = 0;
if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) { if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) {
Song song(song_playing_); Song song(song_playing_);

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,13 +22,9 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QVariant> #include <QVariant>
#include <QByteArray> #include <QByteArray>
#include <QString> #include <QString>
#include <QTimer>
#include "includes/shared_ptr.h" #include "includes/shared_ptr.h"
#include "core/song.h" #include "core/song.h"
@@ -36,6 +32,7 @@
#include "scrobblercache.h" #include "scrobblercache.h"
#include "scrobblercacheitem.h" #include "scrobblercacheitem.h"
class QTimer;
class QNetworkReply; class QNetworkReply;
class ScrobblerSettingsService; class ScrobblerSettingsService;
@@ -53,15 +50,19 @@ class ScrobblingAPI20 : public ScrobblerService {
void ReloadSettings() override; void ReloadSettings() override;
void LoadSession(); void LoadSession();
void ClearSession();
bool enabled() const override { return enabled_; } bool enabled() const override { return enabled_; }
bool authentication_required() const override { return true; }
bool authenticated() const override { return !username_.isEmpty() && !session_key_.isEmpty(); } bool authenticated() const override { return !username_.isEmpty() && !session_key_.isEmpty(); }
bool use_authorization_header() const override { return false; }
QByteArray authorization_header() const override { return QByteArray(); }
bool subscriber() const { return subscriber_; } bool subscriber() const { return subscriber_; }
bool submitted() const override { return submitted_; } bool submitted() const override { return submitted_; }
QString username() const { return username_; } QString username() const { return username_; }
void Authenticate(); void Authenticate();
void Logout();
void UpdateNowPlaying(const Song &song) override; void UpdateNowPlaying(const Song &song) override;
void ClearPlaying() override; void ClearPlaying() override;
void Scrobble(const Song &song) override; void Scrobble(const Song &song) override;
@@ -83,12 +84,6 @@ class ScrobblingAPI20 : public ScrobblerService {
void LoveRequestFinished(QNetworkReply *reply); void LoveRequestFinished(QNetworkReply *reply);
private: private:
enum class ReplyResult {
Success,
ServerError,
APIError
};
enum class ScrobbleErrorCode { enum class ScrobbleErrorCode {
NoError = 1, NoError = 1,
InvalidService = 2, InvalidService = 2,
@@ -121,12 +116,11 @@ class ScrobblingAPI20 : public ScrobblerService {
}; };
QNetworkReply *CreateRequest(const ParamList &request_params); QNetworkReply *CreateRequest(const ParamList &request_params);
ReplyResult GetJsonObject(QNetworkReply *reply, QJsonObject &json_obj, QString &error_description); JsonObjectResult ParseJsonObject(QNetworkReply *reply);
void RequestSession(const QString &token); void RequestSession(const QString &token);
void AuthError(const QString &error); void AuthError(const QString &error);
void SendSingleScrobble(ScrobblerCacheItemPtr item); void SendSingleScrobble(ScrobblerCacheItemPtr item);
void Error(const QString &error, const QVariant &debug = QVariant()); void Error(const QString &error, const QVariant &debug = QVariant()) override;
static QString ErrorString(const ScrobbleErrorCode error); static QString ErrorString(const ScrobbleErrorCode error);
void StartSubmit(const bool initial = false) override; void StartSubmit(const bool initial = false) override;
void CheckScrobblePrevSong(); void CheckScrobblePrevSong();
@@ -140,7 +134,7 @@ class ScrobblingAPI20 : public ScrobblerService {
const SharedPtr<NetworkAccessManager> network_; const SharedPtr<NetworkAccessManager> network_;
ScrobblerCache *cache_; ScrobblerCache *cache_;
LocalRedirectServer *server_; LocalRedirectServer *local_redirect_server_;
bool enabled_; bool enabled_;
bool prefer_albumartist_; bool prefer_albumartist_;
@@ -155,9 +149,7 @@ class ScrobblingAPI20 : public ScrobblerService {
quint64 timestamp_; quint64 timestamp_;
bool submit_error_; bool submit_error_;
QTimer timer_submit_; QTimer *timer_submit_;
QList<QNetworkReply*> replies_;
}; };
#endif // SCROBBLINGAPI20_H #endif // SCROBBLINGAPI20_H

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2020, Pascal Below <spezifisch@below.fr> * Copyright 2020, Pascal Below <spezifisch@below.fr>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
@@ -43,8 +43,8 @@ namespace {
constexpr char kName[] = "Subsonic"; constexpr char kName[] = "Subsonic";
} }
SubsonicScrobbler::SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<SubsonicService> service, QObject *parent) SubsonicScrobbler::SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, const SharedPtr<SubsonicService> service, QObject *parent)
: ScrobblerService(QLatin1String(kName), settings, parent), : ScrobblerService(QLatin1String(kName), network, settings, parent),
service_(service), service_(service),
enabled_(false), enabled_(false),
submitted_(false) { submitted_(false) {

View File

@@ -1,6 +1,6 @@
/* /*
* Strawberry Music Player * Strawberry Music Player
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net> * Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2020, Pascal Below <spezifisch@below.fr> * Copyright 2020, Pascal Below <spezifisch@below.fr>
* *
* Strawberry is free software: you can redistribute it and/or modify * Strawberry is free software: you can redistribute it and/or modify
@@ -23,8 +23,6 @@
#include "config.h" #include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QDateTime> #include <QDateTime>
#include <QVariant> #include <QVariant>
#include <QString> #include <QString>
@@ -41,12 +39,15 @@ class SubsonicScrobbler : public ScrobblerService {
Q_OBJECT Q_OBJECT
public: public:
explicit SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<SubsonicService> service, QObject *parent = nullptr); explicit SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, const SharedPtr<SubsonicService> service, QObject *parent = nullptr);
void ReloadSettings() override; void ReloadSettings() override;
bool enabled() const override { return enabled_; } bool enabled() const override { return enabled_; }
bool authentication_required() const override { return true; }
bool authenticated() const override { return true; } bool authenticated() const override { return true; }
bool use_authorization_header() const override { return false; }
QByteArray authorization_header() const override { return QByteArray(); }
void UpdateNowPlaying(const Song &song) override; void UpdateNowPlaying(const Song &song) override;
void ClearPlaying() override; void ClearPlaying() override;

View File

@@ -190,7 +190,7 @@ void ScrobblerSettingsPage::LastFM_Login() {
void ScrobblerSettingsPage::LastFM_Logout() { void ScrobblerSettingsPage::LastFM_Logout() {
lastfmscrobbler_->Logout(); lastfmscrobbler_->ClearSession();
LastFM_RefreshControls(false); LastFM_RefreshControls(false);
} }
@@ -225,7 +225,7 @@ void ScrobblerSettingsPage::LibreFM_Login() {
void ScrobblerSettingsPage::LibreFM_Logout() { void ScrobblerSettingsPage::LibreFM_Logout() {
librefmscrobbler_->Logout(); librefmscrobbler_->ClearSession();
LibreFM_RefreshControls(false); LibreFM_RefreshControls(false);
} }