Remove libre.fm
This commit is contained in:
@@ -792,9 +792,7 @@ set(SOURCES
|
|||||||
src/scrobbler/scrobblercache.cpp
|
src/scrobbler/scrobblercache.cpp
|
||||||
src/scrobbler/scrobblercacheitem.cpp
|
src/scrobbler/scrobblercacheitem.cpp
|
||||||
src/scrobbler/scrobblemetadata.cpp
|
src/scrobbler/scrobblemetadata.cpp
|
||||||
src/scrobbler/scrobblingapi20.cpp
|
|
||||||
src/scrobbler/lastfmscrobbler.cpp
|
src/scrobbler/lastfmscrobbler.cpp
|
||||||
src/scrobbler/librefmscrobbler.cpp
|
|
||||||
src/scrobbler/listenbrainzscrobbler.cpp
|
src/scrobbler/listenbrainzscrobbler.cpp
|
||||||
src/scrobbler/lastfmimport.cpp
|
src/scrobbler/lastfmimport.cpp
|
||||||
|
|
||||||
@@ -1085,9 +1083,7 @@ set(HEADERS
|
|||||||
src/scrobbler/scrobblersettingsservice.h
|
src/scrobbler/scrobblersettingsservice.h
|
||||||
src/scrobbler/scrobblerservice.h
|
src/scrobbler/scrobblerservice.h
|
||||||
src/scrobbler/scrobblercache.h
|
src/scrobbler/scrobblercache.h
|
||||||
src/scrobbler/scrobblingapi20.h
|
|
||||||
src/scrobbler/lastfmscrobbler.h
|
src/scrobbler/lastfmscrobbler.h
|
||||||
src/scrobbler/librefmscrobbler.h
|
|
||||||
src/scrobbler/listenbrainzscrobbler.h
|
src/scrobbler/listenbrainzscrobbler.h
|
||||||
src/scrobbler/lastfmimport.h
|
src/scrobbler/lastfmimport.h
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Funding developers is a way to contribute to open source projects you appreciate
|
|||||||
* Audio analyzer
|
* Audio analyzer
|
||||||
* Audio equalizer
|
* Audio equalizer
|
||||||
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
* Scrobbler with support for [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||||
* Streaming from Subsonic compatible servers
|
* Streaming from Subsonic compatible servers
|
||||||
* Unofficial Tidal, Spotify and Qobuz integration
|
* Unofficial Tidal, Spotify and Qobuz integration
|
||||||
* Discord rich presence
|
* Discord rich presence
|
||||||
|
|||||||
2
debian/control
vendored
2
debian/control
vendored
@@ -64,7 +64,7 @@ Description: music player and music collection organizer
|
|||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- Unofficial streaming support for Tidal and Qobuz
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
|
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
|
||||||
<li>Audio analyzer and equalizer</li>
|
<li>Audio analyzer and equalizer</li>
|
||||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||||
<li>Streaming support for Subsonic-compatible servers</li>
|
<li>Streaming support for Subsonic-compatible servers</li>
|
||||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
2
dist/unix/strawberry.1
vendored
2
dist/unix/strawberry.1
vendored
@@ -39,7 +39,7 @@ Features:
|
|||||||
.br
|
.br
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
.br
|
.br
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
.br
|
.br
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
.br
|
.br
|
||||||
|
|||||||
2
dist/unix/strawberry.spec.in
vendored
2
dist/unix/strawberry.spec.in
vendored
@@ -97,7 +97,7 @@ Features:
|
|||||||
- Support for multiple backends
|
- Support for multiple backends
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- Unofficial streaming support for Tidal and Qobuz
|
||||||
|
|||||||
@@ -77,7 +77,6 @@
|
|||||||
|
|
||||||
#include "scrobbler/audioscrobbler.h"
|
#include "scrobbler/audioscrobbler.h"
|
||||||
#include "scrobbler/lastfmscrobbler.h"
|
#include "scrobbler/lastfmscrobbler.h"
|
||||||
#include "scrobbler/librefmscrobbler.h"
|
|
||||||
#include "scrobbler/listenbrainzscrobbler.h"
|
#include "scrobbler/listenbrainzscrobbler.h"
|
||||||
#include "scrobbler/lastfmimport.h"
|
#include "scrobbler/lastfmimport.h"
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
@@ -206,7 +205,6 @@ class ApplicationImpl {
|
|||||||
scrobbler_([app]() {
|
scrobbler_([app]() {
|
||||||
AudioScrobbler *scrobbler = new AudioScrobbler(app);
|
AudioScrobbler *scrobbler = new AudioScrobbler(app);
|
||||||
scrobbler->AddService(make_shared<LastFMScrobbler>(scrobbler->settings(), app->network()));
|
scrobbler->AddService(make_shared<LastFMScrobbler>(scrobbler->settings(), app->network()));
|
||||||
scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network()));
|
|
||||||
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
|
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
|
|
||||||
#include "lastfmimport.h"
|
#include "lastfmimport.h"
|
||||||
|
|
||||||
#include "scrobblingapi20.h"
|
|
||||||
#include "lastfmscrobbler.h"
|
#include "lastfmscrobbler.h"
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
@@ -109,7 +108,7 @@ void LastFMImport::ReloadSettings() {
|
|||||||
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
|
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
|
||||||
|
|
||||||
ParamList params = ParamList()
|
ParamList params = ParamList()
|
||||||
<< Param(u"api_key"_s, QLatin1String(ScrobblingAPI20::kApiKey))
|
<< Param(u"api_key"_s, QLatin1String(LastFMScrobbler::kApiKey))
|
||||||
<< Param(u"user"_s, username_)
|
<< Param(u"user"_s, username_)
|
||||||
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
||||||
<< Param(u"format"_s, u"json"_s)
|
<< Param(u"format"_s, u"json"_s)
|
||||||
|
|||||||
@@ -19,20 +19,968 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonValue>
|
||||||
|
#include <QFlags>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
#include "core/networkaccessmanager.h"
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "core/song.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "core/localredirectserver.h"
|
||||||
|
#include "constants/timeconstants.h"
|
||||||
|
#include "constants/scrobblersettings.h"
|
||||||
|
|
||||||
#include "scrobblersettingsservice.h"
|
#include "scrobblersettingsservice.h"
|
||||||
|
#include "scrobblerservice.h"
|
||||||
|
#include "scrobblercache.h"
|
||||||
|
#include "scrobblercacheitem.h"
|
||||||
|
#include "scrobblemetadata.h"
|
||||||
#include "lastfmscrobbler.h"
|
#include "lastfmscrobbler.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
const char *LastFMScrobbler::kName = "Last.fm";
|
const char *LastFMScrobbler::kName = "Last.fm";
|
||||||
const char *LastFMScrobbler::kSettingsGroup = "LastFM";
|
const char *LastFMScrobbler::kSettingsGroup = "LastFM";
|
||||||
const char *LastFMScrobbler::kApiUrl = "https://ws.audioscrobbler.com/2.0/";
|
const char *LastFMScrobbler::kApiUrl = "https://ws.audioscrobbler.com/2.0/";
|
||||||
|
const char *LastFMScrobbler::kApiKey = "211990b4c96782c05d1536e7219eb56e";
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kAuthUrl[] = "https://www.last.fm/api/auth/";
|
constexpr char kAuthUrl[] = "https://www.last.fm/api/auth/";
|
||||||
|
constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8";
|
||||||
|
constexpr int kScrobblesPerRequest = 50;
|
||||||
constexpr char kCacheFile[] = "lastfmscrobbler.cache";
|
constexpr char kCacheFile[] = "lastfmscrobbler.cache";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
LastFMScrobbler::LastFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
LastFMScrobbler::LastFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||||
: ScrobblingAPI20(QLatin1String(kName), QLatin1String(kSettingsGroup), QLatin1String(kAuthUrl), QLatin1String(kApiUrl), true, QLatin1String(kCacheFile), settings, network, parent) {}
|
: ScrobblerService(QLatin1String(kName), network, settings, parent),
|
||||||
|
network_(network),
|
||||||
|
cache_(new ScrobblerCache(QLatin1String(kCacheFile), this)),
|
||||||
|
local_redirect_server_(nullptr),
|
||||||
|
enabled_(false),
|
||||||
|
prefer_albumartist_(false),
|
||||||
|
subscriber_(false),
|
||||||
|
submitted_(false),
|
||||||
|
scrobbled_(false),
|
||||||
|
timestamp_(0),
|
||||||
|
submit_error_(false),
|
||||||
|
timer_submit_(new QTimer(this)) {
|
||||||
|
|
||||||
|
timer_submit_->setSingleShot(true);
|
||||||
|
QObject::connect(timer_submit_, &QTimer::timeout, this, &LastFMScrobbler::Submit);
|
||||||
|
|
||||||
|
LastFMScrobbler::ReloadSettings();
|
||||||
|
LoadSession();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LastFMScrobbler::~LastFMScrobbler() {
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::ReloadSettings() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
s.beginGroup(ScrobblerSettings::kSettingsGroup);
|
||||||
|
prefer_albumartist_ = s.value(ScrobblerSettings::kAlbumArtist, false).toBool();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::LoadSession() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
subscriber_ = s.value("subscriber", false).toBool();
|
||||||
|
username_ = s.value("username").toString();
|
||||||
|
session_key_ = s.value("session_key").toString();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::ClearSession() {
|
||||||
|
|
||||||
|
subscriber_ = false;
|
||||||
|
username_.clear();
|
||||||
|
session_key_.clear();
|
||||||
|
|
||||||
|
Settings settings;
|
||||||
|
settings.beginGroup(kSettingsGroup);
|
||||||
|
settings.remove("subscriber");
|
||||||
|
settings.remove("username");
|
||||||
|
settings.remove("session_key");
|
||||||
|
settings.endGroup();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::Authenticate() {
|
||||||
|
|
||||||
|
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(local_redirect_server_, &LocalRedirectServer::Finished, this, &LastFMScrobbler::RedirectArrived);
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrlQuery url_query;
|
||||||
|
url_query.addQueryItem(u"api_key"_s, QLatin1String(kApiKey));
|
||||||
|
url_query.addQueryItem(u"cb"_s, local_redirect_server_->url().toString());
|
||||||
|
QUrl url(QString::fromLatin1(kAuthUrl));
|
||||||
|
url.setQuery(url_query);
|
||||||
|
|
||||||
|
QMessageBox messagebox(QMessageBox::Information, tr("%1 Scrobbler Authentication").arg(name_), tr("Open URL in web browser?") + QStringLiteral("<br /><a href=\"%1\">%1</a><br />").arg(url.toString()) + tr("Press \"Save\" to copy the URL to clipboard and manually open it in a web browser."), QMessageBox::Open|QMessageBox::Save|QMessageBox::Cancel);
|
||||||
|
messagebox.setTextFormat(Qt::RichText);
|
||||||
|
int result = messagebox.exec();
|
||||||
|
switch (result) {
|
||||||
|
case QMessageBox::Open:{
|
||||||
|
bool openurl_result = QDesktopServices::openUrl(url);
|
||||||
|
if (openurl_result) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
QMessageBox messagebox_error(QMessageBox::Warning, tr("%1 Scrobbler Authentication").arg(name_), tr("Could not open URL. Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
||||||
|
messagebox_error.setTextFormat(Qt::RichText);
|
||||||
|
messagebox_error.exec();
|
||||||
|
}
|
||||||
|
[[fallthrough]];
|
||||||
|
case QMessageBox::Save:
|
||||||
|
QApplication::clipboard()->setText(url.toString());
|
||||||
|
break;
|
||||||
|
case QMessageBox::Cancel:
|
||||||
|
if (local_redirect_server_) {
|
||||||
|
local_redirect_server_->close();
|
||||||
|
local_redirect_server_->deleteLater();
|
||||||
|
local_redirect_server_ = nullptr;
|
||||||
|
}
|
||||||
|
Q_EMIT AuthenticationComplete(false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::RedirectArrived() {
|
||||||
|
|
||||||
|
if (!local_redirect_server_) return;
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
QString token = url_query.queryItemValue(u"token"_s);
|
||||||
|
RequestSession(token);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AuthError(tr("Invalid reply from web browser. Missing token."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AuthError(tr("Received invalid reply from web browser. Try another browser."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AuthError(local_redirect_server_->error());
|
||||||
|
}
|
||||||
|
|
||||||
|
local_redirect_server_->close();
|
||||||
|
local_redirect_server_->deleteLater();
|
||||||
|
local_redirect_server_ = nullptr;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::RequestSession(const QString &token) {
|
||||||
|
|
||||||
|
QUrl session_url(QString::fromLatin1(kApiUrl));
|
||||||
|
QUrlQuery session_url_query;
|
||||||
|
session_url_query.addQueryItem(u"api_key"_s, QLatin1String(kApiKey));
|
||||||
|
session_url_query.addQueryItem(u"method"_s, u"auth.getSession"_s);
|
||||||
|
session_url_query.addQueryItem(u"token"_s, token);
|
||||||
|
QString data_to_sign;
|
||||||
|
const ParamList params = session_url_query.queryItems();
|
||||||
|
for (const Param ¶m : params) {
|
||||||
|
data_to_sign += param.first + param.second;
|
||||||
|
}
|
||||||
|
data_to_sign += QLatin1String(kSecret);
|
||||||
|
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
|
||||||
|
const QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, u'0').toLower();
|
||||||
|
session_url_query.addQueryItem(u"api_sig"_s, signature);
|
||||||
|
session_url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s)));
|
||||||
|
session_url.setQuery(session_url_query);
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreateGetRequest(session_url);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::AuthenticateReplyFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
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_object.contains("session"_L1)) {
|
||||||
|
AuthError(u"Json reply from server is missing session."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue json_value_session = json_object["session"_L1];
|
||||||
|
if (!json_value_session.isObject()) {
|
||||||
|
AuthError(u"Json session is not an object."_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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_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_object_session["subscriber"_L1].toBool();
|
||||||
|
username_ = json_object_session["name"_L1].toString();
|
||||||
|
session_key_ = json_object_session["key"_L1].toString();
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.setValue("subscriber", subscriber_);
|
||||||
|
s.setValue("username", username_);
|
||||||
|
s.setValue("session_key", session_key_);
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
Q_EMIT AuthenticationComplete(true);
|
||||||
|
|
||||||
|
StartSubmit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *LastFMScrobbler::CreateRequest(const ParamList &request_params) {
|
||||||
|
|
||||||
|
ParamList params = ParamList()
|
||||||
|
<< Param(u"api_key"_s, QLatin1String(kApiKey))
|
||||||
|
<< Param(u"sk"_s, session_key_)
|
||||||
|
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
||||||
|
<< request_params;
|
||||||
|
|
||||||
|
std::sort(params.begin(), params.end());
|
||||||
|
|
||||||
|
QUrlQuery url_query;
|
||||||
|
QString data_to_sign;
|
||||||
|
for (const Param ¶m : std::as_const(params)) {
|
||||||
|
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||||
|
url_query.addQueryItem(QString::fromLatin1(encoded_param.first), QString::fromLatin1(encoded_param.second));
|
||||||
|
data_to_sign += param.first + param.second;
|
||||||
|
}
|
||||||
|
data_to_sign += QLatin1String(kSecret);
|
||||||
|
|
||||||
|
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
|
||||||
|
const QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, u'0').toLower();
|
||||||
|
|
||||||
|
url_query.addQueryItem(u"api_sig"_s, QString::fromLatin1(QUrl::toPercentEncoding(signature)));
|
||||||
|
url_query.addQueryItem(u"format"_s, u"json"_s);
|
||||||
|
|
||||||
|
return CreatePostRequest(QUrl(QString::fromLatin1(kApiUrl)), url_query);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonBaseRequest::JsonObjectResult LastFMScrobbler::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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::UpdateNowPlaying(const Song &song) {
|
||||||
|
|
||||||
|
CheckScrobblePrevSong();
|
||||||
|
|
||||||
|
song_playing_ = song;
|
||||||
|
timestamp_ = static_cast<quint64>(QDateTime::currentSecsSinceEpoch());
|
||||||
|
scrobbled_ = false;
|
||||||
|
|
||||||
|
if (!authenticated() || !song.is_metadata_good() || settings_->offline()) return;
|
||||||
|
|
||||||
|
ParamList params = ParamList()
|
||||||
|
<< Param(u"method"_s, u"track.updateNowPlaying"_s)
|
||||||
|
<< Param(u"artist"_s, prefer_albumartist_ ? song.effective_albumartist() : song.artist())
|
||||||
|
<< Param(u"track"_s, StripTitle(song.title()));
|
||||||
|
|
||||||
|
if (!song.album().isEmpty()) {
|
||||||
|
params << Param(u"album"_s, StripAlbum(song.album()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prefer_albumartist_ && !song.albumartist().isEmpty()) {
|
||||||
|
params << Param(u"albumArtist"_s, song.albumartist());
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { UpdateNowPlayingRequestFinished(reply); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
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_object.contains("nowplaying"_L1)) {
|
||||||
|
Error(u"Json reply from server is missing nowplaying."_s, json_object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::ClearPlaying() {
|
||||||
|
|
||||||
|
CheckScrobblePrevSong();
|
||||||
|
|
||||||
|
song_playing_ = Song();
|
||||||
|
scrobbled_ = false;
|
||||||
|
timestamp_ = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::Scrobble(const Song &song) {
|
||||||
|
|
||||||
|
if (song.id() != song_playing_.id() || song.url() != song_playing_.url() || !song.is_metadata_good()) return;
|
||||||
|
|
||||||
|
scrobbled_ = true;
|
||||||
|
|
||||||
|
cache_->Add(song, timestamp_);
|
||||||
|
|
||||||
|
if (settings_->offline()) return;
|
||||||
|
|
||||||
|
if (!authenticated()) {
|
||||||
|
if (settings_->show_error_dialog()) {
|
||||||
|
Q_EMIT ErrorMessage(tr("Scrobbler %1 is not authenticated!").arg(name_));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartSubmit(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::StartSubmit(const bool initial) {
|
||||||
|
|
||||||
|
if (!submitted_ && cache_->Count() > 0) {
|
||||||
|
if (initial && settings_->submit_delay() <= 0 && !submit_error_) {
|
||||||
|
if (timer_submit_->isActive()) {
|
||||||
|
timer_submit_->stop();
|
||||||
|
}
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::Submit() {
|
||||||
|
|
||||||
|
if (!enabled() || !authenticated() || settings_->offline()) return;
|
||||||
|
|
||||||
|
qLog(Debug) << name_ << "Submitting scrobbles.";
|
||||||
|
|
||||||
|
ParamList params = ParamList() << Param(u"method"_s, u"track.scrobble"_s);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
const ScrobblerCacheItemPtrList all_cache_items = cache_->List();
|
||||||
|
ScrobblerCacheItemPtrList cache_items_sent;
|
||||||
|
for (ScrobblerCacheItemPtr cache_item : all_cache_items) {
|
||||||
|
if (cache_item->sent) continue;
|
||||||
|
cache_item->sent = true;
|
||||||
|
cache_items_sent << cache_item;
|
||||||
|
params << Param(u"%1[%2]"_s.arg(u"artist"_s).arg(i), prefer_albumartist_ ? cache_item->metadata.effective_albumartist() : cache_item->metadata.artist);
|
||||||
|
params << Param(u"%1[%2]"_s.arg(u"track"_s).arg(i), StripTitle(cache_item->metadata.title));
|
||||||
|
params << Param(u"%1[%2]"_s.arg(u"timestamp"_s).arg(i), QString::number(cache_item->timestamp));
|
||||||
|
params << Param(u"%1[%2]"_s.arg(u"duration"_s).arg(i), QString::number(cache_item->metadata.length_nanosec / kNsecPerSec));
|
||||||
|
if (!cache_item->metadata.album.isEmpty()) {
|
||||||
|
params << Param(u"%1[%2]"_s.arg("album"_L1).arg(i), StripAlbum(cache_item->metadata.album));
|
||||||
|
}
|
||||||
|
if (!prefer_albumartist_ && !cache_item->metadata.albumartist.isEmpty()) {
|
||||||
|
params << Param(u"%1[%2]"_s.arg("albumArtist"_L1).arg(i), cache_item->metadata.albumartist);
|
||||||
|
}
|
||||||
|
if (cache_item->metadata.track > 0) {
|
||||||
|
params << Param(u"%1[%2]"_s.arg("trackNumber"_L1).arg(i), QString::number(cache_item->metadata.track));
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
if (cache_items_sent.count() >= kScrobblesPerRequest) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache_items_sent.count() <= 0) return;
|
||||||
|
|
||||||
|
submitted_ = true;
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, cache_items_sent]() { ScrobbleRequestFinished(reply, cache_items_sent); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
submitted_ = false;
|
||||||
|
|
||||||
|
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_object.contains("scrobbles"_L1)) {
|
||||||
|
Error(u"Json reply from server is missing scrobbles."_s, json_object);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
||||||
|
if (!value_scrobbles.isObject()) {
|
||||||
|
Error(u"Json scrobbles is not an object."_s, json_object);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
||||||
|
if (object_scrobbles.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
||||||
|
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const QJsonObject object_attr = value_attr.toObject();
|
||||||
|
if (object_attr.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 = object_attr["accepted"_L1].toInt();
|
||||||
|
int ignored = object_attr["ignored"_L1].toInt();
|
||||||
|
|
||||||
|
qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
|
||||||
|
|
||||||
|
QJsonArray array_scrobble;
|
||||||
|
|
||||||
|
const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
|
||||||
|
if (value_scrobble.isObject()) {
|
||||||
|
QJsonObject obj_scrobble = value_scrobble.toObject();
|
||||||
|
if (obj_scrobble.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles scrobble object is empty."_s, obj_scrobble);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
array_scrobble.append(obj_scrobble);
|
||||||
|
}
|
||||||
|
else if (value_scrobble.isArray()) {
|
||||||
|
array_scrobble = value_scrobble.toArray();
|
||||||
|
if (array_scrobble.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles scrobble array is empty."_s, value_scrobble);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Error(u"Json scrobbles scrobble is not an object or array."_s, value_scrobble);
|
||||||
|
StartSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QJsonValue &value : std::as_const(array_scrobble)) {
|
||||||
|
|
||||||
|
if (!value.isObject()) {
|
||||||
|
Error(u"Json scrobbles scrobble array value is not an object."_s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QJsonObject json_track = value.toObject();
|
||||||
|
if (json_track.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json_track.contains("artist"_L1) ||
|
||||||
|
!json_track.contains("album"_L1) ||
|
||||||
|
!json_track.contains("albumArtist"_L1) ||
|
||||||
|
!json_track.contains("track"_L1) ||
|
||||||
|
!json_track.contains("timestamp"_L1) ||
|
||||||
|
!json_track.contains("ignoredMessage"_L1)
|
||||||
|
) {
|
||||||
|
Error(u"Json scrobbles scrobble is missing values."_s, json_track);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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 (!object_artist.contains("#text"_L1) || !object_album.contains("#text"_L1) || !object_song.contains("#text"_L1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//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));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
StartSubmit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::SendSingleScrobble(ScrobblerCacheItemPtr item) {
|
||||||
|
|
||||||
|
ParamList params = ParamList()
|
||||||
|
<< Param(u"method"_s, u"track.scrobble"_s)
|
||||||
|
<< Param(u"artist"_s, prefer_albumartist_ ? item->metadata.effective_albumartist() : item->metadata.artist)
|
||||||
|
<< Param(u"track"_s, StripTitle(item->metadata.title))
|
||||||
|
<< Param(u"timestamp"_s, QString::number(item->timestamp))
|
||||||
|
<< Param(u"duration"_s, QString::number(item->metadata.length_nanosec / kNsecPerSec));
|
||||||
|
|
||||||
|
if (!item->metadata.album.isEmpty()) {
|
||||||
|
params << Param(u"album"_s, StripAlbum(item->metadata.album));
|
||||||
|
}
|
||||||
|
if (!prefer_albumartist_ && !item->metadata.albumartist.isEmpty()) {
|
||||||
|
params << Param(u"albumArtist"_s, item->metadata.albumartist);
|
||||||
|
}
|
||||||
|
if (item->metadata.track > 0) {
|
||||||
|
params << Param(u"trackNumber"_s, QString::number(item->metadata.track));
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, item]() { SingleScrobbleRequestFinished(reply, item); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::SingleScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtr cache_item) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
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_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);
|
||||||
|
|
||||||
|
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
||||||
|
if (!value_scrobbles.isObject()) {
|
||||||
|
Error(u"Json scrobbles is not an object."_s, json_object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
||||||
|
if (object_scrobbles.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
||||||
|
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const QJsonObject object_attr = value_attr.toObject();
|
||||||
|
if (object_attr.isEmpty()) {
|
||||||
|
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
|
||||||
|
Error(u"Json scrobbles attr is missing values."_s, object_attr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_object_scrobble);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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_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();
|
||||||
|
const QString song = json_object_song["#text"_L1].toString();
|
||||||
|
|
||||||
|
const int accepted = object_attr["accepted"_L1].toVariant().toInt();
|
||||||
|
if (accepted == 1) {
|
||||||
|
qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Error(QStringLiteral("Scrobble for \"%1\" not accepted").arg(song));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::Love() {
|
||||||
|
|
||||||
|
if (!song_playing_.is_valid() || !song_playing_.is_metadata_good()) return;
|
||||||
|
|
||||||
|
if (!authenticated()) {
|
||||||
|
Q_EMIT OpenSettingsDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qLog(Debug) << name_ << "Sending love for song" << song_playing_.artist() << song_playing_.album() << song_playing_.title();
|
||||||
|
|
||||||
|
ParamList params = ParamList()
|
||||||
|
<< Param(u"method"_s, u"track.love"_s)
|
||||||
|
<< Param(u"artist"_s, prefer_albumartist_ ? song_playing_.effective_albumartist() : song_playing_.artist())
|
||||||
|
<< Param(u"track"_s, song_playing_.title());
|
||||||
|
|
||||||
|
if (!song_playing_.album().isEmpty()) {
|
||||||
|
params << Param(u"album"_s, song_playing_.album());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prefer_albumartist_ && !song_playing_.albumartist().isEmpty()) {
|
||||||
|
params << Param(u"albumArtist"_s, song_playing_.albumartist());
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] { LoveRequestFinished(reply); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::LoveRequestFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
if (!replies_.contains(reply)) return;
|
||||||
|
replies_.removeAll(reply);
|
||||||
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
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_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;
|
||||||
|
}
|
||||||
|
const QJsonObject json_obj_error = json_value.toObject();
|
||||||
|
if (json_obj_error.isEmpty()) {
|
||||||
|
Error(u"Received empty json error object."_s, json_object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (json_obj_error.contains("code"_L1) && json_obj_error.contains("#text"_L1)) {
|
||||||
|
int code = json_obj_error["code"_L1].toInt();
|
||||||
|
QString text = json_obj_error["#text"_L1].toString();
|
||||||
|
QString error_reason = QStringLiteral("%1 (%2)").arg(text).arg(code);
|
||||||
|
Error(error_reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_object.contains("lfm"_L1)) {
|
||||||
|
const QJsonValue json_value = json_object["lfm"_L1];
|
||||||
|
if (json_value.isObject()) {
|
||||||
|
const QJsonObject json_obj_lfm = json_value.toObject();
|
||||||
|
if (json_obj_lfm.contains("status"_L1)) {
|
||||||
|
const QString status = json_obj_lfm["status"_L1].toString();
|
||||||
|
qLog(Debug) << name_ << "Received love status:" << status;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::AuthError(const QString &error) {
|
||||||
|
|
||||||
|
qLog(Error) << name_ << error;
|
||||||
|
Q_EMIT AuthenticationComplete(false, error);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::Error(const QString &error, const QVariant &debug) {
|
||||||
|
|
||||||
|
JsonBaseRequest::Error(error, debug);
|
||||||
|
|
||||||
|
if (settings_->show_error_dialog()) {
|
||||||
|
Q_EMIT ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LastFMScrobbler::ErrorString(const ScrobbleErrorCode error) {
|
||||||
|
|
||||||
|
switch (error) {
|
||||||
|
case ScrobbleErrorCode::NoError:
|
||||||
|
return u"This error does not exist."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidService:
|
||||||
|
return u"Invalid service - This service does not exist."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidMethod:
|
||||||
|
return u"Invalid Method - No method with that name in this package."_s;
|
||||||
|
case ScrobbleErrorCode::AuthenticationFailed:
|
||||||
|
return u"Authentication Failed - You do not have permissions to access the service."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidFormat:
|
||||||
|
return u"Invalid format - This service doesn't exist in that format."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidParameters:
|
||||||
|
return u"Invalid parameters - Your request is missing a required parameter."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidResourceSpecified:
|
||||||
|
return u"Invalid resource specified"_s;
|
||||||
|
case ScrobbleErrorCode::OperationFailed:
|
||||||
|
return u"Operation failed - Most likely the backend service failed. Please try again."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidSessionKey:
|
||||||
|
return u"Invalid session key - Please re-authenticate."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidApiKey:
|
||||||
|
return u"Invalid API key - You must be granted a valid key by last.fm."_s;
|
||||||
|
case ScrobbleErrorCode::ServiceOffline:
|
||||||
|
return u"Service Offline - This service is temporarily offline. Try again later."_s;
|
||||||
|
case ScrobbleErrorCode::SubscribersOnly:
|
||||||
|
return u"Subscribers Only - This station is only available to paid last.fm subscribers."_s;
|
||||||
|
case ScrobbleErrorCode::InvalidMethodSignature:
|
||||||
|
return u"Invalid method signature supplied."_s;
|
||||||
|
case ScrobbleErrorCode::UnauthorizedToken:
|
||||||
|
return u"Unauthorized Token - This token has not been authorized."_s;
|
||||||
|
case ScrobbleErrorCode::ItemUnavailable:
|
||||||
|
return u"This item is not available for streaming."_s;
|
||||||
|
case ScrobbleErrorCode::TemporarilyUnavailable:
|
||||||
|
return u"The service is temporarily unavailable, please try again."_s;
|
||||||
|
case ScrobbleErrorCode::LoginRequired:
|
||||||
|
return u"Login: User requires to be logged in."_s;
|
||||||
|
case ScrobbleErrorCode::TrialExpired:
|
||||||
|
return u"Trial Expired - This user has no free radio plays left. Subscription required."_s;
|
||||||
|
case ScrobbleErrorCode::ErrorDoesNotExist:
|
||||||
|
return u"This error does not exist."_s;
|
||||||
|
case ScrobbleErrorCode::NotEnoughContent:
|
||||||
|
return u"Not Enough Content - There is not enough content to play this station."_s;
|
||||||
|
case ScrobbleErrorCode::NotEnoughMembers:
|
||||||
|
return u"Not Enough Members - This group does not have enough members for radio."_s;
|
||||||
|
case ScrobbleErrorCode::NotEnoughFans:
|
||||||
|
return u"Not Enough Fans - This artist does not have enough fans for for radio."_s;
|
||||||
|
case ScrobbleErrorCode::NotEnoughNeighbours:
|
||||||
|
return u"Not Enough Neighbours - There are not enough neighbours for radio."_s;
|
||||||
|
case ScrobbleErrorCode::NoPeakRadio:
|
||||||
|
return u"No Peak Radio - This user is not allowed to listen to radio during peak usage."_s;
|
||||||
|
case ScrobbleErrorCode::RadioNotFound:
|
||||||
|
return u"Radio Not Found - Radio station not found."_s;
|
||||||
|
case ScrobbleErrorCode::APIKeySuspended:
|
||||||
|
return u"Suspended API key - Access for your account has been suspended, please contact Last.fm"_s;
|
||||||
|
case ScrobbleErrorCode::Deprecated:
|
||||||
|
return u"Deprecated - This type of request is no longer supported."_s;
|
||||||
|
case ScrobbleErrorCode::RateLimitExceeded:
|
||||||
|
return u"Rate limit exceeded - Your IP has made too many requests in a short period."_s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return u"Unknown error."_s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void LastFMScrobbler::CheckScrobblePrevSong() {
|
||||||
|
|
||||||
|
const qint64 duration = std::max(0LL, QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_));
|
||||||
|
|
||||||
|
if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) {
|
||||||
|
Song song(song_playing_);
|
||||||
|
song.set_length_nanosec(duration * kNsecPerSec);
|
||||||
|
Scrobble(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,22 +22,131 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
#include "scrobblingapi20.h"
|
#include "core/song.h"
|
||||||
|
#include "scrobblerservice.h"
|
||||||
|
#include "scrobblercache.h"
|
||||||
|
#include "scrobblercacheitem.h"
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
class ScrobblerSettingsService;
|
class ScrobblerSettingsService;
|
||||||
class NetworkAccessManager;
|
class NetworkAccessManager;
|
||||||
|
class LocalRedirectServer;
|
||||||
|
|
||||||
class LastFMScrobbler : public ScrobblingAPI20 {
|
class LastFMScrobbler : public ScrobblerService {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LastFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
explicit LastFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||||
|
~LastFMScrobbler() override;
|
||||||
|
|
||||||
static const char *kName;
|
static const char *kName;
|
||||||
static const char *kSettingsGroup;
|
static const char *kSettingsGroup;
|
||||||
static const char *kApiUrl;
|
static const char *kApiUrl;
|
||||||
|
static const char *kApiKey;
|
||||||
|
|
||||||
|
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 UpdateNowPlaying(const Song &song) override;
|
||||||
|
void ClearPlaying() override;
|
||||||
|
void Scrobble(const Song &song) override;
|
||||||
|
void Submit() override;
|
||||||
|
void Love() override;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void AuthenticationComplete(const bool success, const QString &error = QString());
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void WriteCache() override { cache_->WriteCache(); }
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void RedirectArrived();
|
||||||
|
void AuthenticateReplyFinished(QNetworkReply *reply);
|
||||||
|
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
|
||||||
|
void ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items);
|
||||||
|
void SingleScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtr cache_item);
|
||||||
|
void LoveRequestFinished(QNetworkReply *reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class ScrobbleErrorCode {
|
||||||
|
NoError = 1,
|
||||||
|
InvalidService = 2,
|
||||||
|
InvalidMethod = 3,
|
||||||
|
AuthenticationFailed = 4,
|
||||||
|
InvalidFormat = 5,
|
||||||
|
InvalidParameters = 6,
|
||||||
|
InvalidResourceSpecified = 7,
|
||||||
|
OperationFailed = 8,
|
||||||
|
InvalidSessionKey = 9,
|
||||||
|
InvalidApiKey = 10,
|
||||||
|
ServiceOffline = 11,
|
||||||
|
SubscribersOnly = 12,
|
||||||
|
InvalidMethodSignature = 13,
|
||||||
|
UnauthorizedToken = 14,
|
||||||
|
ItemUnavailable = 15,
|
||||||
|
TemporarilyUnavailable = 16,
|
||||||
|
LoginRequired = 17,
|
||||||
|
TrialExpired = 18,
|
||||||
|
ErrorDoesNotExist = 19,
|
||||||
|
NotEnoughContent = 20,
|
||||||
|
NotEnoughMembers = 21,
|
||||||
|
NotEnoughFans = 22,
|
||||||
|
NotEnoughNeighbours = 23,
|
||||||
|
NoPeakRadio = 24,
|
||||||
|
RadioNotFound = 25,
|
||||||
|
APIKeySuspended = 26,
|
||||||
|
Deprecated = 27,
|
||||||
|
RateLimitExceeded = 29,
|
||||||
|
};
|
||||||
|
|
||||||
|
QNetworkReply *CreateRequest(const ParamList &request_params);
|
||||||
|
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()) override;
|
||||||
|
static QString ErrorString(const ScrobbleErrorCode error);
|
||||||
|
void StartSubmit(const bool initial = false) override;
|
||||||
|
void CheckScrobblePrevSong();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
ScrobblerCache *cache_;
|
||||||
|
LocalRedirectServer *local_redirect_server_;
|
||||||
|
|
||||||
|
bool enabled_;
|
||||||
|
bool prefer_albumartist_;
|
||||||
|
|
||||||
|
bool subscriber_;
|
||||||
|
QString username_;
|
||||||
|
QString session_key_;
|
||||||
|
|
||||||
|
bool submitted_;
|
||||||
|
Song song_playing_;
|
||||||
|
bool scrobbled_;
|
||||||
|
quint64 timestamp_;
|
||||||
|
bool submit_error_;
|
||||||
|
|
||||||
|
QTimer *timer_submit_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LASTFMSCROBBLER_H
|
#endif // LASTFMSCROBBLER_H
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* 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
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
|
||||||
#include "core/networkaccessmanager.h"
|
|
||||||
|
|
||||||
#include "scrobblersettingsservice.h"
|
|
||||||
#include "scrobblingapi20.h"
|
|
||||||
#include "librefmscrobbler.h"
|
|
||||||
|
|
||||||
const char *LibreFMScrobbler::kName = "Libre.fm";
|
|
||||||
const char *LibreFMScrobbler::kSettingsGroup = "LibreFM";
|
|
||||||
const char *LibreFMScrobbler::kAuthUrl = "https://www.libre.fm/api/auth/";
|
|
||||||
const char *LibreFMScrobbler::kApiUrl = "https://libre.fm/2.0/";
|
|
||||||
const char *LibreFMScrobbler::kCacheFile = "librefmscrobbler.cache";
|
|
||||||
|
|
||||||
LibreFMScrobbler::LibreFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
|
||||||
: ScrobblingAPI20(QLatin1String(kName), QLatin1String(kSettingsGroup), QLatin1String(kAuthUrl), QLatin1String(kApiUrl), false, QLatin1String(kCacheFile), settings, network, parent) {}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* 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
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef LIBREFMSCROBBLER_H
|
|
||||||
#define LIBREFMSCROBBLER_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
|
||||||
#include "scrobblingapi20.h"
|
|
||||||
|
|
||||||
class ScrobblerSettingsService;
|
|
||||||
class NetworkAccessManager;
|
|
||||||
|
|
||||||
class LibreFMScrobbler : public ScrobblingAPI20 {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit LibreFMScrobbler(const SharedPtr<ScrobblerSettingsService> settings, const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
static const char *kName;
|
|
||||||
static const char *kSettingsGroup;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static const char *kAuthUrl;
|
|
||||||
static const char *kApiUrl;
|
|
||||||
static const char *kCacheFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // LIBREFMSCROBBLER_H
|
|
||||||
@@ -1,990 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* 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
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QLocale>
|
|
||||||
#include <QClipboard>
|
|
||||||
#include <QPair>
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QString>
|
|
||||||
#include <QUrl>
|
|
||||||
#include <QUrlQuery>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QSettings>
|
|
||||||
#include <QNetworkRequest>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonValue>
|
|
||||||
#include <QFlags>
|
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
|
||||||
#include "core/networkaccessmanager.h"
|
|
||||||
#include "core/song.h"
|
|
||||||
#include "core/logging.h"
|
|
||||||
#include "core/settings.h"
|
|
||||||
#include "core/localredirectserver.h"
|
|
||||||
#include "constants/timeconstants.h"
|
|
||||||
#include "constants/scrobblersettings.h"
|
|
||||||
|
|
||||||
#include "scrobblersettingsservice.h"
|
|
||||||
#include "scrobblerservice.h"
|
|
||||||
#include "scrobblingapi20.h"
|
|
||||||
#include "scrobblercache.h"
|
|
||||||
#include "scrobblercacheitem.h"
|
|
||||||
#include "scrobblemetadata.h"
|
|
||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
|
||||||
|
|
||||||
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, network, settings, parent),
|
|
||||||
name_(name),
|
|
||||||
settings_group_(settings_group),
|
|
||||||
auth_url_(auth_url),
|
|
||||||
api_url_(api_url),
|
|
||||||
batch_(batch),
|
|
||||||
network_(network),
|
|
||||||
cache_(new ScrobblerCache(cache_file, this)),
|
|
||||||
local_redirect_server_(nullptr),
|
|
||||||
enabled_(false),
|
|
||||||
prefer_albumartist_(false),
|
|
||||||
subscriber_(false),
|
|
||||||
submitted_(false),
|
|
||||||
scrobbled_(false),
|
|
||||||
timestamp_(0),
|
|
||||||
submit_error_(false),
|
|
||||||
timer_submit_(new QTimer(this)) {
|
|
||||||
|
|
||||||
timer_submit_->setSingleShot(true);
|
|
||||||
QObject::connect(timer_submit_, &QTimer::timeout, this, &ScrobblingAPI20::Submit);
|
|
||||||
|
|
||||||
ScrobblingAPI20::ReloadSettings();
|
|
||||||
LoadSession();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingAPI20::~ScrobblingAPI20() {
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::ReloadSettings() {
|
|
||||||
|
|
||||||
Settings s;
|
|
||||||
|
|
||||||
s.beginGroup(settings_group_);
|
|
||||||
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
s.beginGroup(ScrobblerSettings::kSettingsGroup);
|
|
||||||
prefer_albumartist_ = s.value(ScrobblerSettings::kAlbumArtist, false).toBool();
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::LoadSession() {
|
|
||||||
|
|
||||||
Settings s;
|
|
||||||
s.beginGroup(settings_group_);
|
|
||||||
subscriber_ = s.value("subscriber", false).toBool();
|
|
||||||
username_ = s.value("username").toString();
|
|
||||||
session_key_ = s.value("session_key").toString();
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::ClearSession() {
|
|
||||||
|
|
||||||
subscriber_ = false;
|
|
||||||
username_.clear();
|
|
||||||
session_key_.clear();
|
|
||||||
|
|
||||||
Settings settings;
|
|
||||||
settings.beginGroup(settings_group_);
|
|
||||||
settings.remove("subscriber");
|
|
||||||
settings.remove("username");
|
|
||||||
settings.remove("session_key");
|
|
||||||
settings.endGroup();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::Authenticate() {
|
|
||||||
|
|
||||||
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(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, local_redirect_server_->url().toString());
|
|
||||||
QUrl url(auth_url_);
|
|
||||||
url.setQuery(url_query);
|
|
||||||
|
|
||||||
QMessageBox messagebox(QMessageBox::Information, tr("%1 Scrobbler Authentication").arg(name_), tr("Open URL in web browser?") + QStringLiteral("<br /><a href=\"%1\">%1</a><br />").arg(url.toString()) + tr("Press \"Save\" to copy the URL to clipboard and manually open it in a web browser."), QMessageBox::Open|QMessageBox::Save|QMessageBox::Cancel);
|
|
||||||
messagebox.setTextFormat(Qt::RichText);
|
|
||||||
int result = messagebox.exec();
|
|
||||||
switch (result) {
|
|
||||||
case QMessageBox::Open:{
|
|
||||||
bool openurl_result = QDesktopServices::openUrl(url);
|
|
||||||
if (openurl_result) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
QMessageBox messagebox_error(QMessageBox::Warning, tr("%1 Scrobbler Authentication").arg(name_), tr("Could not open URL. Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
|
|
||||||
messagebox_error.setTextFormat(Qt::RichText);
|
|
||||||
messagebox_error.exec();
|
|
||||||
}
|
|
||||||
[[fallthrough]];
|
|
||||||
case QMessageBox::Save:
|
|
||||||
QApplication::clipboard()->setText(url.toString());
|
|
||||||
break;
|
|
||||||
case QMessageBox::Cancel:
|
|
||||||
if (local_redirect_server_) {
|
|
||||||
local_redirect_server_->close();
|
|
||||||
local_redirect_server_->deleteLater();
|
|
||||||
local_redirect_server_ = nullptr;
|
|
||||||
}
|
|
||||||
Q_EMIT AuthenticationComplete(false);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::RedirectArrived() {
|
|
||||||
|
|
||||||
if (!local_redirect_server_) return;
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
QString token = url_query.queryItemValue(u"token"_s);
|
|
||||||
RequestSession(token);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AuthError(tr("Invalid reply from web browser. Missing token."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AuthError(tr("Received invalid reply from web browser. Try another browser."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AuthError(local_redirect_server_->error());
|
|
||||||
}
|
|
||||||
|
|
||||||
local_redirect_server_->close();
|
|
||||||
local_redirect_server_->deleteLater();
|
|
||||||
local_redirect_server_ = nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::RequestSession(const QString &token) {
|
|
||||||
|
|
||||||
QUrl session_url(api_url_);
|
|
||||||
QUrlQuery session_url_query;
|
|
||||||
session_url_query.addQueryItem(u"api_key"_s, QLatin1String(kApiKey));
|
|
||||||
session_url_query.addQueryItem(u"method"_s, u"auth.getSession"_s);
|
|
||||||
session_url_query.addQueryItem(u"token"_s, token);
|
|
||||||
QString data_to_sign;
|
|
||||||
const ParamList params = session_url_query.queryItems();
|
|
||||||
for (const Param ¶m : params) {
|
|
||||||
data_to_sign += param.first + param.second;
|
|
||||||
}
|
|
||||||
data_to_sign += QLatin1String(kSecret);
|
|
||||||
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
|
|
||||||
const QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, u'0').toLower();
|
|
||||||
session_url_query.addQueryItem(u"api_sig"_s, signature);
|
|
||||||
session_url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s)));
|
|
||||||
session_url.setQuery(session_url_query);
|
|
||||||
|
|
||||||
QNetworkReply *reply = CreateGetRequest(session_url);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { AuthenticateReplyFinished(reply); });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::AuthenticateReplyFinished(QNetworkReply *reply) {
|
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
|
||||||
replies_.removeAll(reply);
|
|
||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
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_object.contains("session"_L1)) {
|
|
||||||
AuthError(u"Json reply from server is missing session."_s);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonValue json_value_session = json_object["session"_L1];
|
|
||||||
if (!json_value_session.isObject()) {
|
|
||||||
AuthError(u"Json session is not an object."_s);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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_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_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_);
|
|
||||||
s.setValue("subscriber", subscriber_);
|
|
||||||
s.setValue("username", username_);
|
|
||||||
s.setValue("session_key", session_key_);
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
Q_EMIT AuthenticationComplete(true);
|
|
||||||
|
|
||||||
StartSubmit();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *ScrobblingAPI20::CreateRequest(const ParamList &request_params) {
|
|
||||||
|
|
||||||
ParamList params = ParamList()
|
|
||||||
<< Param(u"api_key"_s, QLatin1String(kApiKey))
|
|
||||||
<< Param(u"sk"_s, session_key_)
|
|
||||||
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
|
||||||
<< request_params;
|
|
||||||
|
|
||||||
std::sort(params.begin(), params.end());
|
|
||||||
|
|
||||||
QUrlQuery url_query;
|
|
||||||
QString data_to_sign;
|
|
||||||
for (const Param ¶m : std::as_const(params)) {
|
|
||||||
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
|
||||||
url_query.addQueryItem(QString::fromLatin1(encoded_param.first), QString::fromLatin1(encoded_param.second));
|
|
||||||
data_to_sign += param.first + param.second;
|
|
||||||
}
|
|
||||||
data_to_sign += QLatin1String(kSecret);
|
|
||||||
|
|
||||||
QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5);
|
|
||||||
const QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, u'0').toLower();
|
|
||||||
|
|
||||||
url_query.addQueryItem(u"api_sig"_s, QString::fromLatin1(QUrl::toPercentEncoding(signature)));
|
|
||||||
url_query.addQueryItem(u"format"_s, u"json"_s);
|
|
||||||
|
|
||||||
return CreatePostRequest(QUrl(api_url_), url_query);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::UpdateNowPlaying(const Song &song) {
|
|
||||||
|
|
||||||
CheckScrobblePrevSong();
|
|
||||||
|
|
||||||
song_playing_ = song;
|
|
||||||
timestamp_ = static_cast<quint64>(QDateTime::currentSecsSinceEpoch());
|
|
||||||
scrobbled_ = false;
|
|
||||||
|
|
||||||
if (!authenticated() || !song.is_metadata_good() || settings_->offline()) return;
|
|
||||||
|
|
||||||
ParamList params = ParamList()
|
|
||||||
<< Param(u"method"_s, u"track.updateNowPlaying"_s)
|
|
||||||
<< Param(u"artist"_s, prefer_albumartist_ ? song.effective_albumartist() : song.artist())
|
|
||||||
<< Param(u"track"_s, StripTitle(song.title()));
|
|
||||||
|
|
||||||
if (!song.album().isEmpty()) {
|
|
||||||
params << Param(u"album"_s, StripAlbum(song.album()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefer_albumartist_ && !song.albumartist().isEmpty()) {
|
|
||||||
params << Param(u"albumArtist"_s, song.albumartist());
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { UpdateNowPlayingRequestFinished(reply); });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::UpdateNowPlayingRequestFinished(QNetworkReply *reply) {
|
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
|
||||||
replies_.removeAll(reply);
|
|
||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
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_object.contains("nowplaying"_L1)) {
|
|
||||||
Error(u"Json reply from server is missing nowplaying."_s, json_object);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::ClearPlaying() {
|
|
||||||
|
|
||||||
CheckScrobblePrevSong();
|
|
||||||
|
|
||||||
song_playing_ = Song();
|
|
||||||
scrobbled_ = false;
|
|
||||||
timestamp_ = 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::Scrobble(const Song &song) {
|
|
||||||
|
|
||||||
if (song.id() != song_playing_.id() || song.url() != song_playing_.url() || !song.is_metadata_good()) return;
|
|
||||||
|
|
||||||
scrobbled_ = true;
|
|
||||||
|
|
||||||
cache_->Add(song, timestamp_);
|
|
||||||
|
|
||||||
if (settings_->offline()) return;
|
|
||||||
|
|
||||||
if (!authenticated()) {
|
|
||||||
if (settings_->show_error_dialog()) {
|
|
||||||
Q_EMIT ErrorMessage(tr("Scrobbler %1 is not authenticated!").arg(name_));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StartSubmit(true);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
Submit();
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::Submit() {
|
|
||||||
|
|
||||||
if (!enabled() || !authenticated() || settings_->offline()) return;
|
|
||||||
|
|
||||||
qLog(Debug) << name_ << "Submitting scrobbles.";
|
|
||||||
|
|
||||||
ParamList params = ParamList() << Param(u"method"_s, u"track.scrobble"_s);
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
const ScrobblerCacheItemPtrList all_cache_items = cache_->List();
|
|
||||||
ScrobblerCacheItemPtrList cache_items_sent;
|
|
||||||
for (ScrobblerCacheItemPtr cache_item : all_cache_items) {
|
|
||||||
if (cache_item->sent) continue;
|
|
||||||
cache_item->sent = true;
|
|
||||||
if (!batch_) {
|
|
||||||
SendSingleScrobble(cache_item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cache_items_sent << cache_item;
|
|
||||||
params << Param(u"%1[%2]"_s.arg(u"artist"_s).arg(i), prefer_albumartist_ ? cache_item->metadata.effective_albumartist() : cache_item->metadata.artist);
|
|
||||||
params << Param(u"%1[%2]"_s.arg(u"track"_s).arg(i), StripTitle(cache_item->metadata.title));
|
|
||||||
params << Param(u"%1[%2]"_s.arg(u"timestamp"_s).arg(i), QString::number(cache_item->timestamp));
|
|
||||||
params << Param(u"%1[%2]"_s.arg(u"duration"_s).arg(i), QString::number(cache_item->metadata.length_nanosec / kNsecPerSec));
|
|
||||||
if (!cache_item->metadata.album.isEmpty()) {
|
|
||||||
params << Param(u"%1[%2]"_s.arg("album"_L1).arg(i), StripAlbum(cache_item->metadata.album));
|
|
||||||
}
|
|
||||||
if (!prefer_albumartist_ && !cache_item->metadata.albumartist.isEmpty()) {
|
|
||||||
params << Param(u"%1[%2]"_s.arg("albumArtist"_L1).arg(i), cache_item->metadata.albumartist);
|
|
||||||
}
|
|
||||||
if (cache_item->metadata.track > 0) {
|
|
||||||
params << Param(u"%1[%2]"_s.arg("trackNumber"_L1).arg(i), QString::number(cache_item->metadata.track));
|
|
||||||
}
|
|
||||||
++i;
|
|
||||||
if (cache_items_sent.count() >= kScrobblesPerRequest) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!batch_ || cache_items_sent.count() <= 0) return;
|
|
||||||
|
|
||||||
submitted_ = true;
|
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, cache_items_sent]() { ScrobbleRequestFinished(reply, cache_items_sent); });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items) {
|
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
|
||||||
replies_.removeAll(reply);
|
|
||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
submitted_ = false;
|
|
||||||
|
|
||||||
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_object.contains("scrobbles"_L1)) {
|
|
||||||
Error(u"Json reply from server is missing scrobbles."_s, json_object);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
|
||||||
if (!value_scrobbles.isObject()) {
|
|
||||||
Error(u"Json scrobbles is not an object."_s, json_object);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
|
||||||
if (object_scrobbles.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
|
||||||
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const QJsonObject object_attr = value_attr.toObject();
|
|
||||||
if (object_attr.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 = object_attr["accepted"_L1].toInt();
|
|
||||||
int ignored = object_attr["ignored"_L1].toInt();
|
|
||||||
|
|
||||||
qLog(Debug) << name_ << "Scrobbles accepted:" << accepted << "ignored:" << ignored;
|
|
||||||
|
|
||||||
QJsonArray array_scrobble;
|
|
||||||
|
|
||||||
const QJsonValue value_scrobble = object_scrobbles["scrobble"_L1];
|
|
||||||
if (value_scrobble.isObject()) {
|
|
||||||
QJsonObject obj_scrobble = value_scrobble.toObject();
|
|
||||||
if (obj_scrobble.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles scrobble object is empty."_s, obj_scrobble);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
array_scrobble.append(obj_scrobble);
|
|
||||||
}
|
|
||||||
else if (value_scrobble.isArray()) {
|
|
||||||
array_scrobble = value_scrobble.toArray();
|
|
||||||
if (array_scrobble.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles scrobble array is empty."_s, value_scrobble);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Error(u"Json scrobbles scrobble is not an object or array."_s, value_scrobble);
|
|
||||||
StartSubmit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const QJsonValue &value : std::as_const(array_scrobble)) {
|
|
||||||
|
|
||||||
if (!value.isObject()) {
|
|
||||||
Error(u"Json scrobbles scrobble array value is not an object."_s);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
QJsonObject json_track = value.toObject();
|
|
||||||
if (json_track.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!json_track.contains("artist"_L1) ||
|
|
||||||
!json_track.contains("album"_L1) ||
|
|
||||||
!json_track.contains("albumArtist"_L1) ||
|
|
||||||
!json_track.contains("track"_L1) ||
|
|
||||||
!json_track.contains("timestamp"_L1) ||
|
|
||||||
!json_track.contains("ignoredMessage"_L1)
|
|
||||||
) {
|
|
||||||
Error(u"Json scrobbles scrobble is missing values."_s, json_track);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (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 (!object_artist.contains("#text"_L1) || !object_album.contains("#text"_L1) || !object_song.contains("#text"_L1)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//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));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
StartSubmit();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::SendSingleScrobble(ScrobblerCacheItemPtr item) {
|
|
||||||
|
|
||||||
ParamList params = ParamList()
|
|
||||||
<< Param(u"method"_s, u"track.scrobble"_s)
|
|
||||||
<< Param(u"artist"_s, prefer_albumartist_ ? item->metadata.effective_albumartist() : item->metadata.artist)
|
|
||||||
<< Param(u"track"_s, StripTitle(item->metadata.title))
|
|
||||||
<< Param(u"timestamp"_s, QString::number(item->timestamp))
|
|
||||||
<< Param(u"duration"_s, QString::number(item->metadata.length_nanosec / kNsecPerSec));
|
|
||||||
|
|
||||||
if (!item->metadata.album.isEmpty()) {
|
|
||||||
params << Param(u"album"_s, StripAlbum(item->metadata.album));
|
|
||||||
}
|
|
||||||
if (!prefer_albumartist_ && !item->metadata.albumartist.isEmpty()) {
|
|
||||||
params << Param(u"albumArtist"_s, item->metadata.albumartist);
|
|
||||||
}
|
|
||||||
if (item->metadata.track > 0) {
|
|
||||||
params << Param(u"trackNumber"_s, QString::number(item->metadata.track));
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, item]() { SingleScrobbleRequestFinished(reply, item); });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::SingleScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtr cache_item) {
|
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
|
||||||
replies_.removeAll(reply);
|
|
||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
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_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);
|
|
||||||
|
|
||||||
const QJsonValue value_scrobbles = json_object["scrobbles"_L1];
|
|
||||||
if (!value_scrobbles.isObject()) {
|
|
||||||
Error(u"Json scrobbles is not an object."_s, json_object);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const QJsonObject object_scrobbles = value_scrobbles.toObject();
|
|
||||||
if (object_scrobbles.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles object is empty."_s, value_scrobbles);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!object_scrobbles.contains("@attr"_L1) || !object_scrobbles.contains("scrobble"_L1)) {
|
|
||||||
Error(u"Json scrobbles object is missing values."_s, object_scrobbles);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const QJsonObject object_attr = value_attr.toObject();
|
|
||||||
if (object_attr.isEmpty()) {
|
|
||||||
Error(u"Json scrobbles attr is empty."_s, value_attr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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 (!object_attr.contains("accepted"_L1) || !object_attr.contains("ignored"_L1)) {
|
|
||||||
Error(u"Json scrobbles attr is missing values."_s, object_attr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_object_scrobble);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_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_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();
|
|
||||||
const QString song = json_object_song["#text"_L1].toString();
|
|
||||||
|
|
||||||
const int accepted = object_attr["accepted"_L1].toVariant().toInt();
|
|
||||||
if (accepted == 1) {
|
|
||||||
qLog(Debug) << name_ << "Scrobble for" << song << "accepted";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Error(QStringLiteral("Scrobble for \"%1\" not accepted").arg(song));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::Love() {
|
|
||||||
|
|
||||||
if (!song_playing_.is_valid() || !song_playing_.is_metadata_good()) return;
|
|
||||||
|
|
||||||
if (!authenticated()) {
|
|
||||||
Q_EMIT OpenSettingsDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qLog(Debug) << name_ << "Sending love for song" << song_playing_.artist() << song_playing_.album() << song_playing_.title();
|
|
||||||
|
|
||||||
ParamList params = ParamList()
|
|
||||||
<< Param(u"method"_s, u"track.love"_s)
|
|
||||||
<< Param(u"artist"_s, prefer_albumartist_ ? song_playing_.effective_albumartist() : song_playing_.artist())
|
|
||||||
<< Param(u"track"_s, song_playing_.title());
|
|
||||||
|
|
||||||
if (!song_playing_.album().isEmpty()) {
|
|
||||||
params << Param(u"album"_s, song_playing_.album());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prefer_albumartist_ && !song_playing_.albumartist().isEmpty()) {
|
|
||||||
params << Param(u"albumArtist"_s, song_playing_.albumartist());
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] { LoveRequestFinished(reply); });
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::LoveRequestFinished(QNetworkReply *reply) {
|
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
|
||||||
replies_.removeAll(reply);
|
|
||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
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_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;
|
|
||||||
}
|
|
||||||
const QJsonObject json_obj_error = json_value.toObject();
|
|
||||||
if (json_obj_error.isEmpty()) {
|
|
||||||
Error(u"Received empty json error object."_s, json_object);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (json_obj_error.contains("code"_L1) && json_obj_error.contains("#text"_L1)) {
|
|
||||||
int code = json_obj_error["code"_L1].toInt();
|
|
||||||
QString text = json_obj_error["#text"_L1].toString();
|
|
||||||
QString error_reason = QStringLiteral("%1 (%2)").arg(text).arg(code);
|
|
||||||
Error(error_reason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json_object.contains("lfm"_L1)) {
|
|
||||||
const QJsonValue json_value = json_object["lfm"_L1];
|
|
||||||
if (json_value.isObject()) {
|
|
||||||
const QJsonObject json_obj_lfm = json_value.toObject();
|
|
||||||
if (json_obj_lfm.contains("status"_L1)) {
|
|
||||||
const QString status = json_obj_lfm["status"_L1].toString();
|
|
||||||
qLog(Debug) << name_ << "Received love status:" << status;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::AuthError(const QString &error) {
|
|
||||||
|
|
||||||
qLog(Error) << name_ << error;
|
|
||||||
Q_EMIT AuthenticationComplete(false, error);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) {
|
|
||||||
|
|
||||||
JsonBaseRequest::Error(error, debug);
|
|
||||||
|
|
||||||
if (settings_->show_error_dialog()) {
|
|
||||||
Q_EMIT ErrorMessage(tr("Scrobbler %1 error: %2").arg(name_, error));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) {
|
|
||||||
|
|
||||||
switch (error) {
|
|
||||||
case ScrobbleErrorCode::NoError:
|
|
||||||
return u"This error does not exist."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidService:
|
|
||||||
return u"Invalid service - This service does not exist."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidMethod:
|
|
||||||
return u"Invalid Method - No method with that name in this package."_s;
|
|
||||||
case ScrobbleErrorCode::AuthenticationFailed:
|
|
||||||
return u"Authentication Failed - You do not have permissions to access the service."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidFormat:
|
|
||||||
return u"Invalid format - This service doesn't exist in that format."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidParameters:
|
|
||||||
return u"Invalid parameters - Your request is missing a required parameter."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidResourceSpecified:
|
|
||||||
return u"Invalid resource specified"_s;
|
|
||||||
case ScrobbleErrorCode::OperationFailed:
|
|
||||||
return u"Operation failed - Most likely the backend service failed. Please try again."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidSessionKey:
|
|
||||||
return u"Invalid session key - Please re-authenticate."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidApiKey:
|
|
||||||
return u"Invalid API key - You must be granted a valid key by last.fm."_s;
|
|
||||||
case ScrobbleErrorCode::ServiceOffline:
|
|
||||||
return u"Service Offline - This service is temporarily offline. Try again later."_s;
|
|
||||||
case ScrobbleErrorCode::SubscribersOnly:
|
|
||||||
return u"Subscribers Only - This station is only available to paid last.fm subscribers."_s;
|
|
||||||
case ScrobbleErrorCode::InvalidMethodSignature:
|
|
||||||
return u"Invalid method signature supplied."_s;
|
|
||||||
case ScrobbleErrorCode::UnauthorizedToken:
|
|
||||||
return u"Unauthorized Token - This token has not been authorized."_s;
|
|
||||||
case ScrobbleErrorCode::ItemUnavailable:
|
|
||||||
return u"This item is not available for streaming."_s;
|
|
||||||
case ScrobbleErrorCode::TemporarilyUnavailable:
|
|
||||||
return u"The service is temporarily unavailable, please try again."_s;
|
|
||||||
case ScrobbleErrorCode::LoginRequired:
|
|
||||||
return u"Login: User requires to be logged in."_s;
|
|
||||||
case ScrobbleErrorCode::TrialExpired:
|
|
||||||
return u"Trial Expired - This user has no free radio plays left. Subscription required."_s;
|
|
||||||
case ScrobbleErrorCode::ErrorDoesNotExist:
|
|
||||||
return u"This error does not exist."_s;
|
|
||||||
case ScrobbleErrorCode::NotEnoughContent:
|
|
||||||
return u"Not Enough Content - There is not enough content to play this station."_s;
|
|
||||||
case ScrobbleErrorCode::NotEnoughMembers:
|
|
||||||
return u"Not Enough Members - This group does not have enough members for radio."_s;
|
|
||||||
case ScrobbleErrorCode::NotEnoughFans:
|
|
||||||
return u"Not Enough Fans - This artist does not have enough fans for for radio."_s;
|
|
||||||
case ScrobbleErrorCode::NotEnoughNeighbours:
|
|
||||||
return u"Not Enough Neighbours - There are not enough neighbours for radio."_s;
|
|
||||||
case ScrobbleErrorCode::NoPeakRadio:
|
|
||||||
return u"No Peak Radio - This user is not allowed to listen to radio during peak usage."_s;
|
|
||||||
case ScrobbleErrorCode::RadioNotFound:
|
|
||||||
return u"Radio Not Found - Radio station not found."_s;
|
|
||||||
case ScrobbleErrorCode::APIKeySuspended:
|
|
||||||
return u"Suspended API key - Access for your account has been suspended, please contact Last.fm"_s;
|
|
||||||
case ScrobbleErrorCode::Deprecated:
|
|
||||||
return u"Deprecated - This type of request is no longer supported."_s;
|
|
||||||
case ScrobbleErrorCode::RateLimitExceeded:
|
|
||||||
return u"Rate limit exceeded - Your IP has made too many requests in a short period."_s;
|
|
||||||
}
|
|
||||||
|
|
||||||
return u"Unknown error."_s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblingAPI20::CheckScrobblePrevSong() {
|
|
||||||
|
|
||||||
const qint64 duration = std::max(0LL, QDateTime::currentSecsSinceEpoch() - static_cast<qint64>(timestamp_));
|
|
||||||
|
|
||||||
if (!scrobbled_ && song_playing_.is_metadata_good() && song_playing_.is_radio() && duration > 30) {
|
|
||||||
Song song(song_playing_);
|
|
||||||
song.set_length_nanosec(duration * kNsecPerSec);
|
|
||||||
Scrobble(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* Strawberry Music Player
|
|
||||||
* 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
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Strawberry is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef SCROBBLINGAPI20_H
|
|
||||||
#define SCROBBLINGAPI20_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <QVariant>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
|
||||||
#include "core/song.h"
|
|
||||||
#include "scrobblerservice.h"
|
|
||||||
#include "scrobblercache.h"
|
|
||||||
#include "scrobblercacheitem.h"
|
|
||||||
|
|
||||||
class QTimer;
|
|
||||||
class QNetworkReply;
|
|
||||||
|
|
||||||
class ScrobblerSettingsService;
|
|
||||||
class NetworkAccessManager;
|
|
||||||
class LocalRedirectServer;
|
|
||||||
|
|
||||||
class ScrobblingAPI20 : public ScrobblerService {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit 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 = nullptr);
|
|
||||||
~ScrobblingAPI20() override;
|
|
||||||
|
|
||||||
static const char *kApiKey;
|
|
||||||
|
|
||||||
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 UpdateNowPlaying(const Song &song) override;
|
|
||||||
void ClearPlaying() override;
|
|
||||||
void Scrobble(const Song &song) override;
|
|
||||||
void Submit() override;
|
|
||||||
void Love() override;
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void AuthenticationComplete(const bool success, const QString &error = QString());
|
|
||||||
|
|
||||||
public Q_SLOTS:
|
|
||||||
void WriteCache() override { cache_->WriteCache(); }
|
|
||||||
|
|
||||||
private Q_SLOTS:
|
|
||||||
void RedirectArrived();
|
|
||||||
void AuthenticateReplyFinished(QNetworkReply *reply);
|
|
||||||
void UpdateNowPlayingRequestFinished(QNetworkReply *reply);
|
|
||||||
void ScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtrList cache_items);
|
|
||||||
void SingleScrobbleRequestFinished(QNetworkReply *reply, ScrobblerCacheItemPtr cache_item);
|
|
||||||
void LoveRequestFinished(QNetworkReply *reply);
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum class ScrobbleErrorCode {
|
|
||||||
NoError = 1,
|
|
||||||
InvalidService = 2,
|
|
||||||
InvalidMethod = 3,
|
|
||||||
AuthenticationFailed = 4,
|
|
||||||
InvalidFormat = 5,
|
|
||||||
InvalidParameters = 6,
|
|
||||||
InvalidResourceSpecified = 7,
|
|
||||||
OperationFailed = 8,
|
|
||||||
InvalidSessionKey = 9,
|
|
||||||
InvalidApiKey = 10,
|
|
||||||
ServiceOffline = 11,
|
|
||||||
SubscribersOnly = 12,
|
|
||||||
InvalidMethodSignature = 13,
|
|
||||||
UnauthorizedToken = 14,
|
|
||||||
ItemUnavailable = 15,
|
|
||||||
TemporarilyUnavailable = 16,
|
|
||||||
LoginRequired = 17,
|
|
||||||
TrialExpired = 18,
|
|
||||||
ErrorDoesNotExist = 19,
|
|
||||||
NotEnoughContent = 20,
|
|
||||||
NotEnoughMembers = 21,
|
|
||||||
NotEnoughFans = 22,
|
|
||||||
NotEnoughNeighbours = 23,
|
|
||||||
NoPeakRadio = 24,
|
|
||||||
RadioNotFound = 25,
|
|
||||||
APIKeySuspended = 26,
|
|
||||||
Deprecated = 27,
|
|
||||||
RateLimitExceeded = 29,
|
|
||||||
};
|
|
||||||
|
|
||||||
QNetworkReply *CreateRequest(const ParamList &request_params);
|
|
||||||
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()) override;
|
|
||||||
static QString ErrorString(const ScrobbleErrorCode error);
|
|
||||||
void StartSubmit(const bool initial = false) override;
|
|
||||||
void CheckScrobblePrevSong();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QString name_;
|
|
||||||
QString settings_group_;
|
|
||||||
QString auth_url_;
|
|
||||||
QString api_url_;
|
|
||||||
bool batch_;
|
|
||||||
|
|
||||||
const SharedPtr<NetworkAccessManager> network_;
|
|
||||||
ScrobblerCache *cache_;
|
|
||||||
LocalRedirectServer *local_redirect_server_;
|
|
||||||
|
|
||||||
bool enabled_;
|
|
||||||
bool prefer_albumartist_;
|
|
||||||
|
|
||||||
bool subscriber_;
|
|
||||||
QString username_;
|
|
||||||
QString session_key_;
|
|
||||||
|
|
||||||
bool submitted_;
|
|
||||||
Song song_playing_;
|
|
||||||
bool scrobbled_;
|
|
||||||
quint64 timestamp_;
|
|
||||||
bool submit_error_;
|
|
||||||
|
|
||||||
QTimer *timer_submit_;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SCROBBLINGAPI20_H
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
#include "scrobbler/audioscrobbler.h"
|
#include "scrobbler/audioscrobbler.h"
|
||||||
#include "scrobbler/lastfmscrobbler.h"
|
#include "scrobbler/lastfmscrobbler.h"
|
||||||
#include "scrobbler/librefmscrobbler.h"
|
|
||||||
#include "scrobbler/listenbrainzscrobbler.h"
|
#include "scrobbler/listenbrainzscrobbler.h"
|
||||||
#include "constants/scrobblersettings.h"
|
#include "constants/scrobblersettings.h"
|
||||||
|
|
||||||
@@ -50,10 +49,8 @@ ScrobblerSettingsPage::ScrobblerSettingsPage(SettingsDialog *dialog, const Share
|
|||||||
ui_(new Ui_ScrobblerSettingsPage),
|
ui_(new Ui_ScrobblerSettingsPage),
|
||||||
scrobbler_(scrobbler),
|
scrobbler_(scrobbler),
|
||||||
lastfmscrobbler_(scrobbler_->Service<LastFMScrobbler>()),
|
lastfmscrobbler_(scrobbler_->Service<LastFMScrobbler>()),
|
||||||
librefmscrobbler_(scrobbler_->Service<LibreFMScrobbler>()),
|
|
||||||
listenbrainzscrobbler_(scrobbler_->Service<ListenBrainzScrobbler>()),
|
listenbrainzscrobbler_(scrobbler_->Service<ListenBrainzScrobbler>()),
|
||||||
lastfm_waiting_for_auth_(false),
|
lastfm_waiting_for_auth_(false),
|
||||||
librefm_waiting_for_auth_(false),
|
|
||||||
listenbrainz_waiting_for_auth_(false) {
|
listenbrainz_waiting_for_auth_(false) {
|
||||||
|
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
@@ -66,13 +63,6 @@ ScrobblerSettingsPage::ScrobblerSettingsPage(SettingsDialog *dialog, const Share
|
|||||||
QObject::connect(ui_->widget_lastfm_login_state, &LoginStateWidget::LogoutClicked, this, &ScrobblerSettingsPage::LastFM_Logout);
|
QObject::connect(ui_->widget_lastfm_login_state, &LoginStateWidget::LogoutClicked, this, &ScrobblerSettingsPage::LastFM_Logout);
|
||||||
ui_->widget_lastfm_login_state->AddCredentialGroup(ui_->widget_lastfm_login);
|
ui_->widget_lastfm_login_state->AddCredentialGroup(ui_->widget_lastfm_login);
|
||||||
|
|
||||||
// Libre.fm
|
|
||||||
QObject::connect(&*librefmscrobbler_, &LibreFMScrobbler::AuthenticationComplete, this, &ScrobblerSettingsPage::LibreFM_AuthenticationComplete);
|
|
||||||
QObject::connect(ui_->button_librefm_login, &QPushButton::clicked, this, &ScrobblerSettingsPage::LibreFM_Login);
|
|
||||||
QObject::connect(ui_->widget_librefm_login_state, &LoginStateWidget::LoginClicked, this, &ScrobblerSettingsPage::LibreFM_Login);
|
|
||||||
QObject::connect(ui_->widget_librefm_login_state, &LoginStateWidget::LogoutClicked, this, &ScrobblerSettingsPage::LibreFM_Logout);
|
|
||||||
ui_->widget_librefm_login_state->AddCredentialGroup(ui_->widget_librefm_login);
|
|
||||||
|
|
||||||
// ListenBrainz
|
// ListenBrainz
|
||||||
QObject::connect(&*listenbrainzscrobbler_, &ListenBrainzScrobbler::AuthenticationComplete, this, &ScrobblerSettingsPage::ListenBrainz_AuthenticationComplete);
|
QObject::connect(&*listenbrainzscrobbler_, &ListenBrainzScrobbler::AuthenticationComplete, this, &ScrobblerSettingsPage::ListenBrainz_AuthenticationComplete);
|
||||||
QObject::connect(ui_->button_listenbrainz_login, &QPushButton::clicked, this, &ScrobblerSettingsPage::ListenBrainz_Login);
|
QObject::connect(ui_->button_listenbrainz_login, &QPushButton::clicked, this, &ScrobblerSettingsPage::ListenBrainz_Login);
|
||||||
@@ -118,9 +108,6 @@ void ScrobblerSettingsPage::Load() {
|
|||||||
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
|
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
|
||||||
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
|
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
|
||||||
|
|
||||||
ui_->checkbox_librefm_enable->setChecked(librefmscrobbler_->enabled());
|
|
||||||
LibreFM_RefreshControls(librefmscrobbler_->authenticated());
|
|
||||||
|
|
||||||
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
|
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
|
||||||
ui_->lineedit_listenbrainz_user_token->setText(listenbrainzscrobbler_->user_token());
|
ui_->lineedit_listenbrainz_user_token->setText(listenbrainzscrobbler_->user_token());
|
||||||
ListenBrainz_RefreshControls(listenbrainzscrobbler_->authenticated());
|
ListenBrainz_RefreshControls(listenbrainzscrobbler_->authenticated());
|
||||||
@@ -167,10 +154,6 @@ void ScrobblerSettingsPage::Save() {
|
|||||||
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
|
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
s.beginGroup(LibreFMScrobbler::kSettingsGroup);
|
|
||||||
s.setValue(kEnabled, ui_->checkbox_librefm_enable->isChecked());
|
|
||||||
s.endGroup();
|
|
||||||
|
|
||||||
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
|
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
|
||||||
s.setValue(kEnabled, ui_->checkbox_listenbrainz_enable->isChecked());
|
s.setValue(kEnabled, ui_->checkbox_listenbrainz_enable->isChecked());
|
||||||
s.setValue(kUserToken, ui_->lineedit_listenbrainz_user_token->text());
|
s.setValue(kUserToken, ui_->lineedit_listenbrainz_user_token->text());
|
||||||
@@ -215,41 +198,6 @@ void ScrobblerSettingsPage::LastFM_RefreshControls(const bool authenticated) {
|
|||||||
ui_->widget_lastfm_login_state->SetLoggedIn(authenticated ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut, lastfmscrobbler_->username());
|
ui_->widget_lastfm_login_state->SetLoggedIn(authenticated ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut, lastfmscrobbler_->username());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScrobblerSettingsPage::LibreFM_Login() {
|
|
||||||
|
|
||||||
librefm_waiting_for_auth_ = true;
|
|
||||||
ui_->widget_librefm_login_state->SetLoggedIn(LoginStateWidget::State::LoginInProgress);
|
|
||||||
librefmscrobbler_->Authenticate();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblerSettingsPage::LibreFM_Logout() {
|
|
||||||
|
|
||||||
librefmscrobbler_->ClearSession();
|
|
||||||
LibreFM_RefreshControls(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblerSettingsPage::LibreFM_AuthenticationComplete(const bool success, const QString &error) {
|
|
||||||
|
|
||||||
if (!librefm_waiting_for_auth_) return;
|
|
||||||
librefm_waiting_for_auth_ = false;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
QMessageBox::warning(this, u"Authentication failed"_s, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
LibreFM_RefreshControls(success);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblerSettingsPage::LibreFM_RefreshControls(const bool authenticated) {
|
|
||||||
ui_->widget_librefm_login_state->SetLoggedIn(authenticated ? LoginStateWidget::State::LoggedIn : LoginStateWidget::State::LoggedOut, librefmscrobbler_->username());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScrobblerSettingsPage::ListenBrainz_Login() {
|
void ScrobblerSettingsPage::ListenBrainz_Login() {
|
||||||
|
|
||||||
listenbrainz_waiting_for_auth_ = true;
|
listenbrainz_waiting_for_auth_ = true;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -33,7 +33,6 @@ class SettingsDialog;
|
|||||||
class Ui_ScrobblerSettingsPage;
|
class Ui_ScrobblerSettingsPage;
|
||||||
class AudioScrobbler;
|
class AudioScrobbler;
|
||||||
class LastFMScrobbler;
|
class LastFMScrobbler;
|
||||||
class LibreFMScrobbler;
|
|
||||||
class ListenBrainzScrobbler;
|
class ListenBrainzScrobbler;
|
||||||
|
|
||||||
class ScrobblerSettingsPage : public SettingsPage {
|
class ScrobblerSettingsPage : public SettingsPage {
|
||||||
@@ -50,9 +49,6 @@ class ScrobblerSettingsPage : public SettingsPage {
|
|||||||
void LastFM_Login();
|
void LastFM_Login();
|
||||||
void LastFM_Logout();
|
void LastFM_Logout();
|
||||||
void LastFM_AuthenticationComplete(const bool success, const QString &error = QString());
|
void LastFM_AuthenticationComplete(const bool success, const QString &error = QString());
|
||||||
void LibreFM_Login();
|
|
||||||
void LibreFM_Logout();
|
|
||||||
void LibreFM_AuthenticationComplete(const bool success, const QString &error = QString());
|
|
||||||
void ListenBrainz_Login();
|
void ListenBrainz_Login();
|
||||||
void ListenBrainz_Logout();
|
void ListenBrainz_Logout();
|
||||||
void ListenBrainz_AuthenticationComplete(const bool success, const QString &error = QString());
|
void ListenBrainz_AuthenticationComplete(const bool success, const QString &error = QString());
|
||||||
@@ -62,15 +58,12 @@ class ScrobblerSettingsPage : public SettingsPage {
|
|||||||
|
|
||||||
const SharedPtr<AudioScrobbler> scrobbler_;
|
const SharedPtr<AudioScrobbler> scrobbler_;
|
||||||
const SharedPtr<LastFMScrobbler> lastfmscrobbler_;
|
const SharedPtr<LastFMScrobbler> lastfmscrobbler_;
|
||||||
const SharedPtr<LibreFMScrobbler> librefmscrobbler_;
|
|
||||||
const SharedPtr<ListenBrainzScrobbler> listenbrainzscrobbler_;
|
const SharedPtr<ListenBrainzScrobbler> listenbrainzscrobbler_;
|
||||||
|
|
||||||
bool lastfm_waiting_for_auth_;
|
bool lastfm_waiting_for_auth_;
|
||||||
bool librefm_waiting_for_auth_;
|
|
||||||
bool listenbrainz_waiting_for_auth_;
|
bool listenbrainz_waiting_for_auth_;
|
||||||
|
|
||||||
void LastFM_RefreshControls(const bool authenticated);
|
void LastFM_RefreshControls(const bool authenticated);
|
||||||
void LibreFM_RefreshControls(const bool authenticated);
|
|
||||||
void ListenBrainz_RefreshControls(const bool authenticated);
|
void ListenBrainz_RefreshControls(const bool authenticated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -270,55 +270,6 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="groupbox_librefm">
|
|
||||||
<property name="title">
|
|
||||||
<string>Libre.fm</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="layout_librefm">
|
|
||||||
<item>
|
|
||||||
<widget class="QCheckBox" name="checkbox_librefm_enable">
|
|
||||||
<property name="text">
|
|
||||||
<string>Enable</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="LoginStateWidget" name="widget_librefm_login_state" native="true"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QWidget" name="widget_librefm_login" native="true">
|
|
||||||
<layout class="QVBoxLayout" name="layout_librefm_login">
|
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="layout_librefm_button_login">
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="button_librefm_login">
|
|
||||||
<property name="text">
|
|
||||||
<string>Login</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="spacer_librefm_login">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupbox_listenbrainz">
|
<widget class="QGroupBox" name="groupbox_listenbrainz">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@@ -444,8 +395,6 @@
|
|||||||
<tabstop>checkbox_source_radioparadise</tabstop>
|
<tabstop>checkbox_source_radioparadise</tabstop>
|
||||||
<tabstop>checkbox_lastfm_enable</tabstop>
|
<tabstop>checkbox_lastfm_enable</tabstop>
|
||||||
<tabstop>button_lastfm_login</tabstop>
|
<tabstop>button_lastfm_login</tabstop>
|
||||||
<tabstop>checkbox_librefm_enable</tabstop>
|
|
||||||
<tabstop>button_librefm_login</tabstop>
|
|
||||||
<tabstop>checkbox_listenbrainz_enable</tabstop>
|
<tabstop>checkbox_listenbrainz_enable</tabstop>
|
||||||
<tabstop>lineedit_listenbrainz_user_token</tabstop>
|
<tabstop>lineedit_listenbrainz_user_token</tabstop>
|
||||||
<tabstop>button_listenbrainz_login</tabstop>
|
<tabstop>button_listenbrainz_login</tabstop>
|
||||||
|
|||||||
Reference in New Issue
Block a user