Scrobbler: Refactor
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.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
|
||||
* 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<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
|
||||
#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
|
||||
return scrobbler;
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* 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)
|
||||
: QObject(parent),
|
||||
: JsonBaseRequest(network, parent),
|
||||
network_(network),
|
||||
timer_flush_requests_(new QTimer(this)),
|
||||
lastplayed_(false),
|
||||
@@ -117,19 +117,7 @@ QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
|
||||
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : 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;
|
||||
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(LastFMScrobbler::kApiUrl)), params);
|
||||
|
||||
//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->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(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 {
|
||||
QString error;
|
||||
// See if there is Json data containing "error" and "message" - then use that instead.
|
||||
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);
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject LastFMImport::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@@ -279,72 +246,65 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
QJsonObject json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
QString error_reason = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
|
||||
Error(error_reason);
|
||||
if (!json_object.contains("recenttracks"_L1)) {
|
||||
Error(u"JSON reply from server is missing recenttracks."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("recenttracks"_L1)) {
|
||||
Error(u"JSON reply from server is missing recenttracks."_s, json_obj);
|
||||
if (!json_object["recenttracks"_L1].isObject()) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!json_obj["recenttracks"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: recenttracks is not an object!"_s, json_obj);
|
||||
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);
|
||||
if (!json_object.contains("track"_L1)) {
|
||||
Error(u"JSON reply from server is missing track."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("track"_L1)) {
|
||||
Error(u"JSON reply from server is missing track."_s, json_obj);
|
||||
if (!json_object["@attr"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: @attr is not an object."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj["@attr"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: @attr is not an object."_s, json_obj);
|
||||
if (!json_object["track"_L1].isArray()) {
|
||||
Error(u"Failed to parse JSON: track is not an object."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj["track"_L1].isArray()) {
|
||||
Error(u"Failed to parse JSON: track is not an object."_s, json_obj);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj_attr = json_obj["@attr"_L1].toObject();
|
||||
const QJsonObject obj_attr = json_object["@attr"_L1].toObject();
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
int total = obj_attr["total"_L1].toString().toInt();
|
||||
int pages = obj_attr["totalPages"_L1].toString().toInt();
|
||||
const int total = obj_attr["total"_L1].toString().toInt();
|
||||
const int pages = obj_attr["totalPages"_L1].toString().toInt();
|
||||
|
||||
if (page == 0) {
|
||||
lastplayed_total_ = total;
|
||||
@@ -353,7 +313,7 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -374,19 +334,19 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj_artist = obj_track["artist"_L1].toObject();
|
||||
QJsonObject obj_album = obj_track["album"_L1].toObject();
|
||||
QJsonObject obj_date = obj_track["date"_L1].toObject();
|
||||
const QJsonObject obj_artist = obj_track["artist"_L1].toObject();
|
||||
const QJsonObject obj_album = obj_track["album"_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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString artist = obj_artist["#text"_L1].toString();
|
||||
QString album = obj_album["#text"_L1].toString();
|
||||
QString date = obj_date["#text"_L1].toString();
|
||||
QString title = obj_track["name"_L1].toString();
|
||||
QDateTime datetime = QDateTime::fromString(date, u"dd MMM yyyy, hh:mm"_s);
|
||||
const QString artist = obj_artist["#text"_L1].toString();
|
||||
const QString album = obj_album["#text"_L1].toString();
|
||||
const QString date = obj_date["#text"_L1].toString();
|
||||
const QString title = obj_track["name"_L1].toString();
|
||||
const QDateTime datetime = QDateTime::fromString(date, u"dd MMM yyyy, hh:mm"_s);
|
||||
if (datetime.isValid()) {
|
||||
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);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
QJsonObject json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
QString error_reason = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
|
||||
Error(error_reason);
|
||||
if (!json_object.contains("toptracks"_L1)) {
|
||||
Error(u"JSON reply from server is missing toptracks."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("toptracks"_L1)) {
|
||||
Error(u"JSON reply from server is missing toptracks."_s, json_obj);
|
||||
if (!json_object["toptracks"_L1].isObject()) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!json_obj["toptracks"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: toptracks is not an object!"_s, json_obj);
|
||||
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);
|
||||
if (!json_object.contains("track"_L1)) {
|
||||
Error(u"JSON reply from server is missing track."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("track"_L1)) {
|
||||
Error(u"JSON reply from server is missing track."_s, json_obj);
|
||||
if (!json_object["@attr"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: @attr is not an object."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj["@attr"_L1].isObject()) {
|
||||
Error(u"Failed to parse JSON: @attr is not an object."_s, json_obj);
|
||||
if (!json_object["track"_L1].isArray()) {
|
||||
Error(u"Failed to parse JSON: track is not an object."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj["track"_L1].isArray()) {
|
||||
Error(u"Failed to parse JSON: track is not an object."_s, json_obj);
|
||||
const QJsonObject object_attr = json_object["@attr"_L1].toObject();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
QJsonObject obj_attr = json_obj["@attr"_L1].toObject();
|
||||
|
||||
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();
|
||||
const int pages = object_attr["totalPages"_L1].toString().toInt();
|
||||
const int total = object_attr["total"_L1].toString().toInt();
|
||||
|
||||
if (page == 0) {
|
||||
playcount_total_ = total;
|
||||
@@ -516,8 +469,8 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
|
||||
}
|
||||
else {
|
||||
|
||||
QJsonArray array_track = json_obj["track"_L1].toArray();
|
||||
for (QJsonArray::iterator it = array_track.begin(); it != array_track.end(); ++it) {
|
||||
const QJsonArray array_track = json_object["track"_L1].toArray();
|
||||
for (QJsonArray::ConstIterator it = array_track.begin(); it != array_track.constEnd(); ++it) {
|
||||
|
||||
const QJsonValue &value_track = *it;
|
||||
|
||||
@@ -527,7 +480,7 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj_track = value_track.toObject();
|
||||
const QJsonObject obj_track = value_track.toObject();
|
||||
if (!obj_track.contains("artist"_L1) ||
|
||||
!obj_track.contains("name"_L1) ||
|
||||
!obj_track.contains("playcount"_L1) ||
|
||||
@@ -536,14 +489,14 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj_artist = obj_track["artist"_L1].toObject();
|
||||
const QJsonObject obj_artist = obj_track["artist"_L1].toObject();
|
||||
if (!obj_artist.contains("name"_L1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString artist = obj_artist["name"_L1].toString();
|
||||
QString title = obj_track["name"_L1].toString();
|
||||
int playcount = obj_track["playcount"_L1].toString().toInt();
|
||||
const QString artist = obj_artist["name"_L1].toString();
|
||||
const QString title = obj_track["name"_L1].toString();
|
||||
const int playcount = obj_track["playcount"_L1].toString().toInt();
|
||||
|
||||
if (playcount <= 0) continue;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,8 +22,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
@@ -31,20 +29,28 @@
|
||||
#include <QQueue>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "core/jsonbaserequest.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/jsonbaserequest.h"
|
||||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
class LastFMImport : public QObject {
|
||||
class LastFMImport : public JsonBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~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 ImportData(const bool lastplayed = true, const bool playcount = true);
|
||||
void AbortAll();
|
||||
@@ -64,8 +70,7 @@ class LastFMImport : public QObject {
|
||||
|
||||
private:
|
||||
QNetworkReply *CreateRequest(const ParamList &request_params);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
|
||||
void AddGetRecentTracksRequest(const int page = 0);
|
||||
void AddGetTopTracksRequest(const int page = 0);
|
||||
@@ -73,7 +78,7 @@ class LastFMImport : public QObject {
|
||||
void SendGetRecentTracksRequest(GetRecentTracksRequest 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 UpdateProgressCheck();
|
||||
@@ -86,7 +91,7 @@ class LastFMImport : public QObject {
|
||||
void UpdateTotal(const int, const int);
|
||||
void UpdateProgress(const int, const int);
|
||||
void Finished();
|
||||
void FinishedWithError(const QString&);
|
||||
void FinishedWithError(const QString &error);
|
||||
|
||||
private Q_SLOTS:
|
||||
void FlushRequests();
|
||||
@@ -106,7 +111,6 @@ class LastFMImport : public QObject {
|
||||
int lastplayed_received_;
|
||||
QQueue<GetRecentTracksRequest> recent_tracks_requests_;
|
||||
QQueue<GetTopTracksRequest> top_tracks_requests_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // LASTFMIMPORT_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,9 +22,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "scrobblingapi20.h"
|
||||
|
||||
@@ -40,6 +37,7 @@ class LastFMScrobbler : public ScrobblingAPI20 {
|
||||
static const char *kName;
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kApiUrl;
|
||||
|
||||
};
|
||||
|
||||
#endif // LASTFMSCROBBLER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,9 +22,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "scrobblingapi20.h"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,18 +23,12 @@
|
||||
#include <utility>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QtGlobal>
|
||||
#include <QDesktopServices>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
@@ -46,7 +40,7 @@
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/localredirectserver.h"
|
||||
#include "core/oauthenticator.h"
|
||||
#include "constants/timeconstants.h"
|
||||
#include "constants/scrobblersettings.h"
|
||||
|
||||
@@ -66,6 +60,7 @@ namespace {
|
||||
constexpr char kOAuthAuthorizeUrl[] = "https://musicbrainz.org/oauth2/authorize";
|
||||
constexpr char kOAuthAccessTokenUrl[] = "https://musicbrainz.org/oauth2/token";
|
||||
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 kClientIDB64[] = "b2VBVU53cVNRZXIwZXIwOUZpcWkwUQ==";
|
||||
constexpr char kClientSecretB64[] = "Uk9GZ2hrZVEzRjNvUHlFaHFpeVdQQQ==";
|
||||
@@ -74,44 +69,42 @@ constexpr int kScrobblesPerRequest = 10;
|
||||
} // namespace
|
||||
|
||||
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),
|
||||
oauth_(new OAuthenticator(network, this)),
|
||||
cache_(new ScrobblerCache(QLatin1String(kCacheFile), this)),
|
||||
server_(nullptr),
|
||||
timer_submit_(new QTimer(this)),
|
||||
enabled_(false),
|
||||
expires_in_(-1),
|
||||
login_time_(0),
|
||||
submitted_(false),
|
||||
scrobbled_(false),
|
||||
timestamp_(0),
|
||||
submit_error_(false),
|
||||
prefer_albumartist_(false) {
|
||||
|
||||
refresh_login_timer_.setSingleShot(true);
|
||||
QObject::connect(&refresh_login_timer_, &QTimer::timeout, this, &ListenBrainzScrobbler::RequestNewAccessToken);
|
||||
oauth_->set_settings_group(QLatin1String(kSettingsGroup));
|
||||
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(&timer_submit_, &QTimer::timeout, this, &ListenBrainzScrobbler::Submit);
|
||||
QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &ListenBrainzScrobbler::OAuthFinished);
|
||||
|
||||
timer_submit_->setSingleShot(true);
|
||||
QObject::connect(timer_submit_, &QTimer::timeout, this, &ListenBrainzScrobbler::Submit);
|
||||
|
||||
ListenBrainzScrobbler::ReloadSettings();
|
||||
LoadSession();
|
||||
oauth_->LoadSession();
|
||||
|
||||
}
|
||||
|
||||
ListenBrainzScrobbler::~ListenBrainzScrobbler() {
|
||||
bool ListenBrainzScrobbler::authenticated() const {
|
||||
|
||||
while (!replies_.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();
|
||||
}
|
||||
return !oauth_->access_token().isEmpty() && !user_token_.isEmpty();
|
||||
|
||||
}
|
||||
|
||||
@@ -129,252 +122,41 @@ void ListenBrainzScrobbler::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::LoadSession() {
|
||||
void ListenBrainzScrobbler::Authenticate() {
|
||||
|
||||
Settings s;
|
||||
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();
|
||||
oauth_->Authenticate();
|
||||
|
||||
if (!refresh_token_.isEmpty()) {
|
||||
qint64 time = expires_in_ - (QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(login_time_));
|
||||
if (time < 6) time = 6;
|
||||
refresh_login_timer_.setInterval(static_cast<int>(time * kMsecPerSec));
|
||||
refresh_login_timer_.start();
|
||||
}
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::Deauthenticate() {
|
||||
|
||||
oauth_->ClearSession();
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::Logout() {
|
||||
|
||||
access_token_.clear();
|
||||
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();
|
||||
Deauthenticate();
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::Authenticate() {
|
||||
void ListenBrainzScrobbler::OAuthFinished(const bool success, const QString &error) {
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(this);
|
||||
if (!server_->Listen()) {
|
||||
AuthError(server_->error());
|
||||
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."));
|
||||
}
|
||||
if (success) {
|
||||
qLog(Debug) << "ListenBrainz: Authentication was successful, login expires in" << oauth_->expires_in();
|
||||
Q_EMIT AuthenticationComplete(true);
|
||||
StartSubmit();
|
||||
}
|
||||
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;
|
||||
|
||||
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 ¶m : 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;
|
||||
return CreatePostRequest(url, json_document);
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
CheckScrobblePrevSong();
|
||||
@@ -476,10 +317,9 @@ void ListenBrainzScrobbler::UpdateNowPlaying(const Song &song) {
|
||||
QJsonObject object;
|
||||
object.insert("listen_type"_L1, "playing_now"_L1);
|
||||
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(url, doc);
|
||||
QNetworkReply *reply = CreateRequest(QUrl(QStringLiteral("%1/1/submit-listens").arg(QLatin1String(kApiUrl))), json_document);
|
||||
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);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_description;
|
||||
if (GetJsonObject(reply, json_obj, error_description) != ReplyResult::Success) {
|
||||
Error(error_description);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
QString status = json_obj["status"_L1].toString();
|
||||
const QString status = json_object["status"_L1].toString();
|
||||
if (status.compare("ok"_L1, Qt::CaseInsensitive) != 0) {
|
||||
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 (initial && settings_->submit_delay() <= 0 && !submit_error_) {
|
||||
if (timer_submit_.isActive()) {
|
||||
timer_submit_.stop();
|
||||
if (timer_submit_->isActive()) {
|
||||
timer_submit_->stop();
|
||||
}
|
||||
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);
|
||||
timer_submit_.setInterval(submit_delay);
|
||||
timer_submit_.start();
|
||||
timer_submit_->setInterval(submit_delay);
|
||||
timer_submit_->start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,7 +421,7 @@ void ListenBrainzScrobbler::Submit() {
|
||||
object.insert("payload"_L1, array);
|
||||
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);
|
||||
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;
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
const ReplyResult reply_result = GetJsonObject(reply, json_obj, error_message);
|
||||
if (reply_result == ReplyResult::Success) {
|
||||
if (json_obj.contains("status"_L1)) {
|
||||
QString status = json_obj["status"_L1].toString();
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (json_object_result.success()) {
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (json_object.contains("status"_L1)) {
|
||||
const QString status = json_object["status"_L1].toString();
|
||||
qLog(Debug) << "ListenBrainz: Received scrobble status:" << status;
|
||||
}
|
||||
else {
|
||||
@@ -612,20 +451,20 @@ void ListenBrainzScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, Scrobb
|
||||
}
|
||||
else {
|
||||
submit_error_ = true;
|
||||
if (reply_result == ReplyResult::APIError) {
|
||||
if (json_object_result.error_code == ErrorCode::APIError) {
|
||||
if (cache_items.count() == 1) {
|
||||
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);
|
||||
}
|
||||
else {
|
||||
Error(error_message);
|
||||
Error(json_object_result.error_message);
|
||||
cache_->SetError(cache_items);
|
||||
cache_->ClearSent(cache_items);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Error(error_message);
|
||||
Error(json_object_result.error_message);
|
||||
cache_->ClearSent(cache_items);
|
||||
}
|
||||
}
|
||||
@@ -667,33 +506,25 @@ void ListenBrainzScrobbler::LoveRequestFinished(QNetworkReply *reply) {
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
Error(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
if (json_obj.contains("status"_L1)) {
|
||||
qLog(Debug) << "ListenBrainz: Received recording-feedback status:" << json_obj["status"_L1].toString();
|
||||
if (json_object.contains("status"_L1)) {
|
||||
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;
|
||||
Q_EMIT AuthenticationComplete(false, error);
|
||||
|
||||
}
|
||||
|
||||
void ListenBrainzScrobbler::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "ListenBrainz:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
JsonBaseRequest::Error(error_message, debug_output);
|
||||
|
||||
if (settings_->show_error_dialog()) {
|
||||
Q_EMIT ErrorMessage(tr("ListenBrainz error: %1").arg(error));
|
||||
Q_EMIT ErrorMessage(tr("ListenBrainz error: %1").arg(error_message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,15 +22,11 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
@@ -38,31 +34,34 @@
|
||||
#include "scrobblercache.h"
|
||||
#include "scrobblemetadata.h"
|
||||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
|
||||
class ScrobblerSettingsService;
|
||||
class NetworkAccessManager;
|
||||
class LocalRedirectServer;
|
||||
class OAuthenticator;
|
||||
|
||||
class ListenBrainzScrobbler : public ScrobblerService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ListenBrainzScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~ListenBrainzScrobbler() override;
|
||||
|
||||
static const char *kName;
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
void ReloadSettings() override;
|
||||
void LoadSession();
|
||||
|
||||
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_; }
|
||||
QString user_token() const { return user_token_; }
|
||||
|
||||
void Authenticate();
|
||||
void Deauthenticate();
|
||||
void Logout();
|
||||
void Submit() override;
|
||||
void UpdateNowPlaying(const Song &song) override;
|
||||
@@ -70,12 +69,6 @@ class ListenBrainzScrobbler : public ScrobblerService {
|
||||
void Scrobble(const Song &song) override;
|
||||
void Love() override;
|
||||
|
||||
enum class ReplyResult {
|
||||
Success,
|
||||
ServerError,
|
||||
APIError
|
||||
};
|
||||
|
||||
Q_SIGNALS:
|
||||
void AuthenticationComplete(const bool success, const QString &error = QString());
|
||||
|
||||
@@ -83,44 +76,32 @@ class ListenBrainzScrobbler : public ScrobblerService {
|
||||
void WriteCache() override { cache_->WriteCache(); }
|
||||
|
||||
private Q_SLOTS:
|
||||
void RedirectArrived();
|
||||
void AuthenticateReplyFinished(QNetworkReply *reply);
|
||||
void RequestNewAccessToken() { RequestAccessToken(); }
|
||||
void OAuthFinished(const bool success, const QString &error);
|
||||
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
|
||||
void ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items);
|
||||
void LoveRequestFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_doc);
|
||||
ReplyResult GetJsonObject(QNetworkReply *reply, QJsonObject &json_obj, QString &error_description);
|
||||
QNetworkReply *CreateRequest(const QUrl &url, const QJsonDocument &json_document);
|
||||
QJsonObject JsonTrackMetadata(const ScrobbleMetadata &metadata) const;
|
||||
void AuthError(const QString &error);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
void RequestAccessToken(const QUrl &redirect_url = QUrl(), const QString &code = QString());
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
|
||||
void StartSubmit(const bool initial = false) override;
|
||||
void CheckScrobblePrevSong();
|
||||
|
||||
const SharedPtr<NetworkAccessManager> network_;
|
||||
OAuthenticator *oauth_;
|
||||
ScrobblerCache *cache_;
|
||||
LocalRedirectServer *server_;
|
||||
QTimer *timer_submit_;
|
||||
bool enabled_;
|
||||
QString user_token_;
|
||||
QString access_token_;
|
||||
qint64 expires_in_;
|
||||
QString token_type_;
|
||||
QString refresh_token_;
|
||||
quint64 login_time_;
|
||||
bool submitted_;
|
||||
Song song_playing_;
|
||||
bool scrobbled_;
|
||||
quint64 timestamp_;
|
||||
QTimer refresh_login_timer_;
|
||||
QTimer timer_submit_;
|
||||
bool submit_error_;
|
||||
|
||||
bool prefer_albumartist_;
|
||||
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // LISTENBRAINZSCROBBLER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,37 +19,14 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "scrobblerservice.h"
|
||||
#include "scrobblersettingsservice.h"
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
ScrobblerService::ScrobblerService(const QString &name, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent) : QObject(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;
|
||||
|
||||
}
|
||||
ScrobblerService::ScrobblerService(const QString &name, const SharedPtr<NetworkAccessManager> network, const SharedPtr<ScrobblerSettingsService> settings, QObject *parent) : JsonBaseRequest(network, parent), name_(name), settings_(settings) {}
|
||||
|
||||
QString ScrobblerService::StripAlbum(const QString &album) const {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,31 +22,29 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
#include "core/jsonbaserequest.h"
|
||||
|
||||
#include "scrobblersettingsservice.h"
|
||||
|
||||
class ScrobblerService : public QObject {
|
||||
class ScrobblerService : public JsonBaseRequest {
|
||||
Q_OBJECT
|
||||
|
||||
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 service_name() const override { return name_; }
|
||||
|
||||
virtual void ReloadSettings() = 0;
|
||||
|
||||
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 ClearPlaying() = 0;
|
||||
@@ -57,12 +55,8 @@ class ScrobblerService : public QObject {
|
||||
virtual bool submitted() const { return false; }
|
||||
|
||||
protected:
|
||||
using Param = QPair<QString, QString>;
|
||||
using ParamList = QList<Param>;
|
||||
using EncodedParam = QPair<QByteArray, QByteArray>;
|
||||
|
||||
bool ExtractJsonObj(const QByteArray &data, QJsonObject &json_obj, QString &error_description);
|
||||
|
||||
QString StripAlbum(const QString &album) const;
|
||||
QString StripTitle(const QString &title) const;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -68,10 +68,10 @@ const char *ScrobblingAPI20::kApiKey = "211990b4c96782c05d1536e7219eb56e";
|
||||
namespace {
|
||||
constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8";
|
||||
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)
|
||||
: ScrobblerService(name, settings, parent),
|
||||
: ScrobblerService(name, network, settings, parent),
|
||||
name_(name),
|
||||
settings_group_(settings_group),
|
||||
auth_url_(auth_url),
|
||||
@@ -79,17 +79,18 @@ ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_gr
|
||||
batch_(batch),
|
||||
network_(network),
|
||||
cache_(new ScrobblerCache(cache_file, this)),
|
||||
server_(nullptr),
|
||||
local_redirect_server_(nullptr),
|
||||
enabled_(false),
|
||||
prefer_albumartist_(false),
|
||||
subscriber_(false),
|
||||
submitted_(false),
|
||||
scrobbled_(false),
|
||||
timestamp_(0),
|
||||
submit_error_(false) {
|
||||
submit_error_(false),
|
||||
timer_submit_(new QTimer(this)) {
|
||||
|
||||
timer_submit_.setSingleShot(true);
|
||||
QObject::connect(&timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit);
|
||||
timer_submit_->setSingleShot(true);
|
||||
QObject::connect(timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit);
|
||||
|
||||
ScrobblingAPI20::ReloadSettings();
|
||||
LoadSession();
|
||||
@@ -98,17 +99,10 @@ ScrobblingAPI20::ScrobblingAPI20(const QString &name, const QString &settings_gr
|
||||
|
||||
ScrobblingAPI20::~ScrobblingAPI20() {
|
||||
|
||||
while (!replies_.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();
|
||||
if (local_redirect_server_) {
|
||||
QObject::disconnect(local_redirect_server_, nullptr, this, nullptr);
|
||||
if (local_redirect_server_->isListening()) local_redirect_server_->close();
|
||||
local_redirect_server_->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -138,7 +132,7 @@ void ScrobblingAPI20::LoadSession() {
|
||||
|
||||
}
|
||||
|
||||
void ScrobblingAPI20::Logout() {
|
||||
void ScrobblingAPI20::ClearSession() {
|
||||
|
||||
subscriber_ = false;
|
||||
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() {
|
||||
|
||||
if (!server_) {
|
||||
server_ = new LocalRedirectServer(this);
|
||||
if (!server_->Listen()) {
|
||||
AuthError(server_->error());
|
||||
delete server_;
|
||||
server_ = nullptr;
|
||||
if (!local_redirect_server_) {
|
||||
local_redirect_server_ = new LocalRedirectServer(this);
|
||||
if (!local_redirect_server_->Listen()) {
|
||||
AuthError(local_redirect_server_->error());
|
||||
delete local_redirect_server_;
|
||||
local_redirect_server_ = nullptr;
|
||||
return;
|
||||
}
|
||||
QObject::connect(server_, &LocalRedirectServer::Finished, this, &ScrobblingAPI20::RedirectArrived);
|
||||
QObject::connect(local_redirect_server_, &LocalRedirectServer::Finished, this, &ScrobblingAPI20::RedirectArrived);
|
||||
}
|
||||
|
||||
QUrlQuery url_query;
|
||||
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_);
|
||||
url.setQuery(url_query);
|
||||
|
||||
@@ -238,10 +184,10 @@ void ScrobblingAPI20::Authenticate() {
|
||||
QApplication::clipboard()->setText(url.toString());
|
||||
break;
|
||||
case QMessageBox::Cancel:
|
||||
if (server_) {
|
||||
server_->close();
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
if (local_redirect_server_) {
|
||||
local_redirect_server_->close();
|
||||
local_redirect_server_->deleteLater();
|
||||
local_redirect_server_ = nullptr;
|
||||
}
|
||||
Q_EMIT AuthenticationComplete(false);
|
||||
break;
|
||||
@@ -253,10 +199,10 @@ void ScrobblingAPI20::Authenticate() {
|
||||
|
||||
void ScrobblingAPI20::RedirectArrived() {
|
||||
|
||||
if (!server_) return;
|
||||
if (!local_redirect_server_) return;
|
||||
|
||||
if (server_->error().isEmpty()) {
|
||||
QUrl url = server_->request_url();
|
||||
if (local_redirect_server_->error().isEmpty()) {
|
||||
const QUrl url = local_redirect_server_->request_url();
|
||||
if (url.isValid()) {
|
||||
QUrlQuery url_query(url);
|
||||
if (url_query.hasQueryItem(u"token"_s)) {
|
||||
@@ -272,12 +218,12 @@ void ScrobblingAPI20::RedirectArrived() {
|
||||
}
|
||||
}
|
||||
else {
|
||||
AuthError(server_->error());
|
||||
AuthError(local_redirect_server_->error());
|
||||
}
|
||||
|
||||
server_->close();
|
||||
server_->deleteLater();
|
||||
server_ = nullptr;
|
||||
local_redirect_server_->close();
|
||||
local_redirect_server_->deleteLater();
|
||||
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.setQuery(session_url_query);
|
||||
|
||||
QNetworkRequest req(session_url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
QNetworkReply *reply = CreateGetRequest(session_url);
|
||||
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);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
AuthError(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (json_object_result.error_code != ErrorCode::Success) {
|
||||
AuthError(json_object_result.error_message);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue json_session = json_obj["session"_L1];
|
||||
if (!json_session.isObject()) {
|
||||
const QJsonValue json_value_session = json_object["session"_L1];
|
||||
if (!json_value_session.isObject()) {
|
||||
AuthError(u"Json session is not an object."_s);
|
||||
return;
|
||||
}
|
||||
json_obj = json_session.toObject();
|
||||
if (json_obj.isEmpty()) {
|
||||
const QJsonObject json_object_session = json_value_session.toObject();
|
||||
if (json_object_session.isEmpty()) {
|
||||
AuthError(u"Json session object is empty."_s);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
subscriber_ = json_obj["subscriber"_L1].toBool();
|
||||
username_ = json_obj["name"_L1].toString();
|
||||
session_key_ = json_obj["key"_L1].toString();
|
||||
subscriber_ = json_object_session["subscriber"_L1].toBool();
|
||||
username_ = json_object_session["name"_L1].toString();
|
||||
session_key_ = json_object_session["key"_L1].toString();
|
||||
|
||||
Settings s;
|
||||
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"format"_s, u"json"_s);
|
||||
|
||||
QUrl url(api_url_);
|
||||
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;
|
||||
return CreatePostRequest(QUrl(api_url_), url_query);
|
||||
|
||||
//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);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
Error(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (json_object_result.error_code != ErrorCode::Success) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
if (!json_obj.contains("nowplaying"_L1)) {
|
||||
Error(u"Json reply from server is missing nowplaying."_s, json_obj);
|
||||
if (!json_object.contains("nowplaying"_L1)) {
|
||||
Error(u"Json reply from server is missing nowplaying."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -482,15 +468,15 @@ void ScrobblingAPI20::StartSubmit(const bool initial) {
|
||||
|
||||
if (!submitted_ && cache_->Count() > 0) {
|
||||
if (initial && (!batch_ || settings_->submit_delay() <= 0) && !submit_error_) {
|
||||
if (timer_submit_.isActive()) {
|
||||
timer_submit_.stop();
|
||||
if (timer_submit_->isActive()) {
|
||||
timer_submit_->stop();
|
||||
}
|
||||
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);
|
||||
timer_submit_.setInterval(submit_delay);
|
||||
timer_submit_.start();
|
||||
timer_submit_->setInterval(submit_delay);
|
||||
timer_submit_->start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,68 +536,68 @@ void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCac
|
||||
|
||||
submitted_ = false;
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
Error(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
cache_->ClearSent(cache_items);
|
||||
submit_error_ = true;
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
cache_->Flush(cache_items);
|
||||
submit_error_ = false;
|
||||
|
||||
if (!json_obj.contains("scrobbles"_L1)) {
|
||||
Error(u"Json reply from server is missing scrobbles."_s, json_obj);
|
||||
if (!json_object.contains("scrobbles"_L1)) {
|
||||
Error(u"Json reply from server is missing scrobbles."_s, json_object);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_scrobbles = json_obj["scrobbles"_L1];
|
||||
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
||||
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();
|
||||
return;
|
||||
}
|
||||
json_obj = value_scrobbles.toObject();
|
||||
if (json_obj.isEmpty()) {
|
||||
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
||||
if (object_scrobbles.isEmpty()) {
|
||||
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
if (!json_obj.contains("@attr"_L1) || !json_obj.contains("scrobble"_L1)) {
|
||||
Error(u"Json scrobbles object is missing values."_s, json_obj);
|
||||
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
||||
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_attr = json_obj["@attr"_L1];
|
||||
const QJsonValue value_attr = object_scrobbles["@attr"_L1];
|
||||
if (!value_attr.isObject()) {
|
||||
Error(u"Json scrobbles attr is not an object."_s, value_attr);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_attr = value_attr.toObject();
|
||||
if (obj_attr.isEmpty()) {
|
||||
const QJsonObject object_attr = value_attr.toObject();
|
||||
if (object_attr.isEmpty()) {
|
||||
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
if (!obj_attr.contains("accepted"_L1) || !obj_attr.contains("ignored"_L1)) {
|
||||
Error(u"Json scrobbles attr is missing values."_s, obj_attr);
|
||||
if (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
|
||||
Error(u"Json scrobbles attr is missing values."_s, object_attr);
|
||||
StartSubmit();
|
||||
return;
|
||||
}
|
||||
int accepted = obj_attr["accepted"_L1].toInt();
|
||||
int ignored = obj_attr["ignored"_L1].toInt();
|
||||
int accepted = object_attr["accepted"_L1].toInt();
|
||||
int ignored = object_attr["ignored"_L1].toInt();
|
||||
|
||||
qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
|
||||
|
||||
QJsonArray array_scrobble;
|
||||
|
||||
QJsonValue value_scrobble = json_obj["scrobble"_L1];
|
||||
const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
|
||||
if (value_scrobble.isObject()) {
|
||||
QJsonObject obj_scrobble = value_scrobble.toObject();
|
||||
if (obj_scrobble.isEmpty()) {
|
||||
@@ -657,36 +643,36 @@ void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCac
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonValue value_artist = json_track["artist"_L1];
|
||||
QJsonValue value_album = json_track["album"_L1];
|
||||
QJsonValue value_song = json_track["track"_L1];
|
||||
QJsonValue value_ignoredmessage = json_track["ignoredMessage"_L1];
|
||||
//quint64 timestamp = json_track[u"timestamp"_s].toVariant().toULongLong();
|
||||
const QJsonValue value_artist = json_track["artist"_L1];
|
||||
const QJsonValue value_album = json_track["album"_L1];
|
||||
const QJsonValue value_song = json_track["track"_L1];
|
||||
const QJsonValue value_ignoredmessage = json_track["ignoredMessage"_L1];
|
||||
//const quint64 timestamp = json_track[u"timestamp"_s].toVariant().toULongLong();
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
QJsonObject obj_album = value_album.toObject();
|
||||
QJsonObject obj_song = value_song.toObject();
|
||||
QJsonObject obj_ignoredmessage = value_ignoredmessage.toObject();
|
||||
const QJsonObject object_artist = value_artist.toObject();
|
||||
const QJsonObject object_album = value_album.toObject();
|
||||
const QJsonObject object_song = value_song.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);
|
||||
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;
|
||||
}
|
||||
|
||||
//QString artist = obj_artist["#text"].toString();
|
||||
//QString album = obj_album["#text"].toString();
|
||||
QString song = obj_song["#text"_L1].toString();
|
||||
bool ignoredmessage = obj_ignoredmessage["code"_L1].toVariant().toBool();
|
||||
QString ignoredmessage_text = obj_ignoredmessage["#text"_L1].toString();
|
||||
//const QString artist = obj_artist["#text"].toString();
|
||||
//const QString album = obj_album["#text"].toString();
|
||||
const QString song = object_song["#text"_L1].toString();
|
||||
const bool ignoredmessage = object_ignoredmessage["code"_L1].toVariant().toBool();
|
||||
const QString ignoredmessage_text = object_ignoredmessage["#text"_L1].toString();
|
||||
|
||||
if (ignoredmessage) {
|
||||
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);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
Error(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (json_object_result.error_code != ErrorCode::Success) {
|
||||
Error(json_object_result.error_message);
|
||||
cache_item->sent = false;
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
if (!json_obj.contains("scrobbles"_L1)) {
|
||||
Error(u"Json reply from server is missing scrobbles."_s, json_obj);
|
||||
if (!json_object.contains("scrobbles"_L1)) {
|
||||
Error(u"Json reply from server is missing scrobbles."_s, json_object);
|
||||
cache_item->sent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cache_->Remove(cache_item);
|
||||
|
||||
QJsonValue value_scrobbles = json_obj["scrobbles"_L1];
|
||||
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
||||
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;
|
||||
}
|
||||
json_obj = value_scrobbles.toObject();
|
||||
if (json_obj.isEmpty()) {
|
||||
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
||||
if (object_scrobbles.isEmpty()) {
|
||||
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
||||
return;
|
||||
}
|
||||
if (!json_obj.contains("@attr"_L1) || !json_obj.contains("scrobble"_L1)) {
|
||||
Error(u"Json scrobbles object is missing values."_s, json_obj);
|
||||
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
||||
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_attr = json_obj["@attr"_L1];
|
||||
const QJsonValue value_attr = object_scrobbles["@attr"_L1];
|
||||
if (!value_attr.isObject()) {
|
||||
Error(u"Json scrobbles attr is not an object."_s, value_attr);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_attr = value_attr.toObject();
|
||||
if (obj_attr.isEmpty()) {
|
||||
const QJsonObject object_attr = value_attr.toObject();
|
||||
if (object_attr.isEmpty()) {
|
||||
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_scrobble = json_obj["scrobble"_L1];
|
||||
const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
|
||||
if (!value_scrobble.isObject()) {
|
||||
Error(u"Json scrobbles scrobble is not an object."_s, value_scrobble);
|
||||
return;
|
||||
}
|
||||
QJsonObject json_obj_scrobble = value_scrobble.toObject();
|
||||
if (json_obj_scrobble.isEmpty()) {
|
||||
const QJsonObject json_object_scrobble = value_scrobble.toObject();
|
||||
if (json_object_scrobble.isEmpty()) {
|
||||
Error(u"Json scrobbles scrobble is empty."_s, value_scrobble);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!obj_attr.contains("accepted"_L1) || !obj_attr.contains("ignored"_L1)) {
|
||||
Error(u"Json scrobbles attr is missing values."_s, obj_attr);
|
||||
if (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
|
||||
Error(u"Json scrobbles attr is missing values."_s, object_attr);
|
||||
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)) {
|
||||
Error(u"Json scrobbles scrobble is missing values."_s, json_obj_scrobble);
|
||||
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_object_scrobble);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue json_value_artist = json_obj_scrobble["artist"_L1];
|
||||
QJsonValue json_value_album = json_obj_scrobble["album"_L1];
|
||||
QJsonValue json_value_song = json_obj_scrobble["track"_L1];
|
||||
const QJsonValue json_value_artist = json_object_scrobble["artist"_L1];
|
||||
const QJsonValue json_value_album = json_object_scrobble["album"_L1];
|
||||
const QJsonValue json_value_song = json_object_scrobble["track"_L1];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
QJsonObject json_obj_artist = json_value_artist.toObject();
|
||||
QJsonObject json_obj_album = json_value_album.toObject();
|
||||
QJsonObject json_obj_song = json_value_song.toObject();
|
||||
const QJsonObject json_object_artist = json_value_artist.toObject();
|
||||
const QJsonObject json_object_album = json_value_album.toObject();
|
||||
const QJsonObject json_object_song = json_value_song.toObject();
|
||||
|
||||
if (json_obj_artist.isEmpty() || json_obj_album.isEmpty() || json_obj_song.isEmpty()) {
|
||||
Error(u"Json scrobbles scrobble values objects are empty."_s, json_obj_scrobble);
|
||||
if (json_object_artist.isEmpty() || json_object_album.isEmpty() || json_object_song.isEmpty()) {
|
||||
Error(u"Json scrobbles scrobble values objects are empty."_s, json_object_scrobble);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj_artist.contains("#text"_L1) || !json_obj_album.contains("#text"_L1) || !json_obj_song.contains("#text"_L1)) {
|
||||
Error(u"Json scrobbles scrobble values objects are missing #text."_s, json_obj_artist);
|
||||
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_object_artist);
|
||||
return;
|
||||
}
|
||||
|
||||
//QString artist = json_obj_artist["#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) {
|
||||
qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
|
||||
}
|
||||
@@ -868,22 +854,22 @@ void ScrobblingAPI20::LoveRequestFinished(QNetworkReply *reply) {
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj;
|
||||
QString error_message;
|
||||
if (GetJsonObject(reply, json_obj, error_message) != ReplyResult::Success) {
|
||||
Error(error_message);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (json_object_result.error_code != ErrorCode::Success) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
if (json_obj.contains("error"_L1)) {
|
||||
QJsonValue json_value = json_obj["error"_L1];
|
||||
if (json_object.contains("error"_L1)) {
|
||||
const QJsonValue json_value = json_object["error"_L1];
|
||||
if (!json_value.isObject()) {
|
||||
Error(u"Error is not on object."_s);
|
||||
return;
|
||||
}
|
||||
QJsonObject json_obj_error = json_value.toObject();
|
||||
const QJsonObject json_obj_error = json_value.toObject();
|
||||
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;
|
||||
}
|
||||
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)) {
|
||||
QJsonValue json_value = json_obj["lfm"_L1];
|
||||
if (json_object.contains("lfm"_L1)) {
|
||||
const QJsonValue json_value = json_object["lfm"_L1];
|
||||
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)) {
|
||||
QString status = json_obj_lfm["status"_L1].toString();
|
||||
const QString status = json_obj_lfm["status"_L1].toString();
|
||||
qLog(Debug) << name_ << "Received love status:" << status;
|
||||
return;
|
||||
}
|
||||
@@ -918,8 +904,7 @@ void ScrobblingAPI20::AuthError(const QString &error) {
|
||||
|
||||
void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << name_ << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
JsonBaseRequest::Error(error, debug);
|
||||
|
||||
if (settings_->show_error_dialog()) {
|
||||
Q_EMIT ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error));
|
||||
@@ -994,8 +979,7 @@ QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) {
|
||||
|
||||
void ScrobblingAPI20::CheckScrobblePrevSong() {
|
||||
|
||||
qint64 duration = QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_);
|
||||
if (duration < 0) duration = 0;
|
||||
const qint64 duration = std::min(0LL, QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_));
|
||||
|
||||
if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) {
|
||||
Song song(song_playing_);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,13 +22,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
@@ -36,6 +32,7 @@
|
||||
#include "scrobblercache.h"
|
||||
#include "scrobblercacheitem.h"
|
||||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
|
||||
class ScrobblerSettingsService;
|
||||
@@ -53,15 +50,19 @@ class ScrobblingAPI20 : public ScrobblerService {
|
||||
|
||||
void ReloadSettings() override;
|
||||
void LoadSession();
|
||||
void ClearSession();
|
||||
|
||||
bool enabled() const override { return enabled_; }
|
||||
bool authentication_required() const override { return true; }
|
||||
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 submitted() const override { return submitted_; }
|
||||
QString username() const { return username_; }
|
||||
|
||||
void Authenticate();
|
||||
void Logout();
|
||||
void UpdateNowPlaying(const Song &song) override;
|
||||
void ClearPlaying() override;
|
||||
void Scrobble(const Song &song) override;
|
||||
@@ -83,12 +84,6 @@ class ScrobblingAPI20 : public ScrobblerService {
|
||||
void LoveRequestFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
enum class ReplyResult {
|
||||
Success,
|
||||
ServerError,
|
||||
APIError
|
||||
};
|
||||
|
||||
enum class ScrobbleErrorCode {
|
||||
NoError = 1,
|
||||
InvalidService = 2,
|
||||
@@ -121,12 +116,11 @@ class ScrobblingAPI20 : public ScrobblerService {
|
||||
};
|
||||
|
||||
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 AuthError(const QString &error);
|
||||
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);
|
||||
void StartSubmit(const bool initial = false) override;
|
||||
void CheckScrobblePrevSong();
|
||||
@@ -140,7 +134,7 @@ class ScrobblingAPI20 : public ScrobblerService {
|
||||
|
||||
const SharedPtr<NetworkAccessManager> network_;
|
||||
ScrobblerCache *cache_;
|
||||
LocalRedirectServer *server_;
|
||||
LocalRedirectServer *local_redirect_server_;
|
||||
|
||||
bool enabled_;
|
||||
bool prefer_albumartist_;
|
||||
@@ -155,9 +149,7 @@ class ScrobblingAPI20 : public ScrobblerService {
|
||||
quint64 timestamp_;
|
||||
bool submit_error_;
|
||||
|
||||
QTimer timer_submit_;
|
||||
|
||||
QList<QNetworkReply*> replies_;
|
||||
QTimer *timer_submit_;
|
||||
};
|
||||
|
||||
#endif // SCROBBLINGAPI20_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
@@ -43,8 +43,8 @@ namespace {
|
||||
constexpr char kName[] = "Subsonic";
|
||||
}
|
||||
|
||||
SubsonicScrobbler::SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<SubsonicService> service, QObject *parent)
|
||||
: ScrobblerService(QLatin1String(kName), settings, parent),
|
||||
SubsonicScrobbler::SubsonicScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, const SharedPtr<SubsonicService> service, QObject *parent)
|
||||
: ScrobblerService(QLatin1String(kName), network, settings, parent),
|
||||
service_(service),
|
||||
enabled_(false),
|
||||
submitted_(false) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
@@ -23,8 +23,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
@@ -41,12 +39,15 @@ class SubsonicScrobbler : public ScrobblerService {
|
||||
Q_OBJECT
|
||||
|
||||
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;
|
||||
|
||||
bool enabled() const override { return enabled_; }
|
||||
bool authentication_required() 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 ClearPlaying() override;
|
||||
|
||||
@@ -190,7 +190,7 @@ void ScrobblerSettingsPage::LastFM_Login() {
|
||||
|
||||
void ScrobblerSettingsPage::LastFM_Logout() {
|
||||
|
||||
lastfmscrobbler_->Logout();
|
||||
lastfmscrobbler_->ClearSession();
|
||||
LastFM_RefreshControls(false);
|
||||
|
||||
}
|
||||
@@ -225,7 +225,7 @@ void ScrobblerSettingsPage::LibreFM_Login() {
|
||||
|
||||
void ScrobblerSettingsPage::LibreFM_Logout() {
|
||||
|
||||
librefmscrobbler_->Logout();
|
||||
librefmscrobbler_->ClearSession();
|
||||
LibreFM_RefreshControls(false);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user