diff --git a/CMakeLists.txt b/CMakeLists.txt
index c2fbabd9f..d78a9e7a4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -792,9 +792,7 @@ set(SOURCES
src/scrobbler/scrobblercache.cpp
src/scrobbler/scrobblercacheitem.cpp
src/scrobbler/scrobblemetadata.cpp
- src/scrobbler/scrobblingapi20.cpp
src/scrobbler/lastfmscrobbler.cpp
- src/scrobbler/librefmscrobbler.cpp
src/scrobbler/listenbrainzscrobbler.cpp
src/scrobbler/lastfmimport.cpp
@@ -1085,9 +1083,7 @@ set(HEADERS
src/scrobbler/scrobblersettingsservice.h
src/scrobbler/scrobblerservice.h
src/scrobbler/scrobblercache.h
- src/scrobbler/scrobblingapi20.h
src/scrobbler/lastfmscrobbler.h
- src/scrobbler/librefmscrobbler.h
src/scrobbler/listenbrainzscrobbler.h
src/scrobbler/lastfmimport.h
diff --git a/README.md b/README.md
index 52a7a213b..895ef35e4 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Audio analyzer
* Audio equalizer
* 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
* Unofficial Tidal, Spotify and Qobuz integration
* Discord rich presence
diff --git a/debian/control b/debian/control
index b32a7089c..05035d18c 100644
--- a/debian/control
+++ b/debian/control
@@ -64,7 +64,7 @@ Description: music player and music collection organizer
- Audio analyzer
- Audio equalizer
- 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
- Unofficial streaming support for Tidal and Qobuz
.
diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
index 9572d30a1..9869d08d2 100644
--- a/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
+++ b/dist/unix/org.strawberrymusicplayer.strawberry.appdata.xml
@@ -34,7 +34,7 @@
Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
Audio analyzer and equalizer
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
Unofficial streaming support for Tidal, Spotify and Qobuz
diff --git a/dist/unix/strawberry.1 b/dist/unix/strawberry.1
index 14b16b8a0..17ccf79cf 100644
--- a/dist/unix/strawberry.1
+++ b/dist/unix/strawberry.1
@@ -39,7 +39,7 @@ Features:
.br
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
.br
-- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
+- Scrobbler with support for Last.fm and ListenBrainz
.br
- Streaming support for Subsonic-compatible servers
.br
diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in
index 691caa1e9..ab2218127 100644
--- a/dist/unix/strawberry.spec.in
+++ b/dist/unix/strawberry.spec.in
@@ -97,7 +97,7 @@ Features:
- Support for multiple backends
- Audio analyzer
- 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
- Streaming support for Subsonic-compatible servers
- Unofficial streaming support for Tidal and Qobuz
diff --git a/src/core/application.cpp b/src/core/application.cpp
index a96f64330..b99cadfba 100644
--- a/src/core/application.cpp
+++ b/src/core/application.cpp
@@ -77,7 +77,6 @@
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmscrobbler.h"
-#include "scrobbler/librefmscrobbler.h"
#include "scrobbler/listenbrainzscrobbler.h"
#include "scrobbler/lastfmimport.h"
#ifdef HAVE_SUBSONIC
@@ -206,7 +205,6 @@ class ApplicationImpl {
scrobbler_([app]() {
AudioScrobbler *scrobbler = new AudioScrobbler(app);
scrobbler->AddService(make_shared(scrobbler->settings(), app->network()));
- scrobbler->AddService(make_shared(scrobbler->settings(), app->network()));
scrobbler->AddService(make_shared(scrobbler->settings(), app->network()));
#ifdef HAVE_SUBSONIC
scrobbler->AddService(make_shared(scrobbler->settings(), app->network(), app->streaming_services()->Service(), app));
diff --git a/src/scrobbler/lastfmimport.cpp b/src/scrobbler/lastfmimport.cpp
index 61561d963..a4fba77ea 100644
--- a/src/scrobbler/lastfmimport.cpp
+++ b/src/scrobbler/lastfmimport.cpp
@@ -45,7 +45,6 @@
#include "lastfmimport.h"
-#include "scrobblingapi20.h"
#include "lastfmscrobbler.h"
using namespace Qt::Literals::StringLiterals;
@@ -109,7 +108,7 @@ void LastFMImport::ReloadSettings() {
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
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"lang"_s, QLocale().name().left(2).toLower())
<< Param(u"format"_s, u"json"_s)
diff --git a/src/scrobbler/lastfmscrobbler.cpp b/src/scrobbler/lastfmscrobbler.cpp
index d16bd02fc..746758c30 100644
--- a/src/scrobbler/lastfmscrobbler.cpp
+++ b/src/scrobbler/lastfmscrobbler.cpp
@@ -19,20 +19,968 @@
#include "config.h"
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
#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 "scrobblercache.h"
+#include "scrobblercacheitem.h"
+#include "scrobblemetadata.h"
#include "lastfmscrobbler.h"
+using namespace Qt::Literals::StringLiterals;
+
const char *LastFMScrobbler::kName = "Last.fm";
const char *LastFMScrobbler::kSettingsGroup = "LastFM";
const char *LastFMScrobbler::kApiUrl = "https://ws.audioscrobbler.com/2.0/";
+const char *LastFMScrobbler::kApiKey = "211990b4c96782c05d1536e7219eb56e";
namespace {
constexpr char kAuthUrl[] = "https://www.last.fm/api/auth/";
+constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8";
+constexpr int kScrobblesPerRequest = 50;
constexpr char kCacheFile[] = "lastfmscrobbler.cache";
} // namespace
LastFMScrobbler::LastFMScrobbler(const SharedPtr settings, const SharedPtr 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("
%1
").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(":
%1").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(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(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(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);
+ }
+
+}
diff --git a/src/scrobbler/lastfmscrobbler.h b/src/scrobbler/lastfmscrobbler.h
index 86850b9ba..c8cd5b8fd 100644
--- a/src/scrobbler/lastfmscrobbler.h
+++ b/src/scrobbler/lastfmscrobbler.h
@@ -22,22 +22,131 @@
#include "config.h"
+#include
+#include
+#include
+
#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 NetworkAccessManager;
+class LocalRedirectServer;
-class LastFMScrobbler : public ScrobblingAPI20 {
+class LastFMScrobbler : public ScrobblerService {
Q_OBJECT
public:
explicit LastFMScrobbler(const SharedPtr settings, const SharedPtr network, QObject *parent = nullptr);
+ ~LastFMScrobbler() override;
static const char *kName;
static const char *kSettingsGroup;
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 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
diff --git a/src/scrobbler/librefmscrobbler.cpp b/src/scrobbler/librefmscrobbler.cpp
deleted file mode 100644
index 2d7cb468f..000000000
--- a/src/scrobbler/librefmscrobbler.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Strawberry Music Player
- * Copyright 2018-2025, Jonas Kvinge
- *
- * 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 .
- *
- */
-
-#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 settings, const SharedPtr network, QObject *parent)
- : ScrobblingAPI20(QLatin1String(kName), QLatin1String(kSettingsGroup), QLatin1String(kAuthUrl), QLatin1String(kApiUrl), false, QLatin1String(kCacheFile), settings, network, parent) {}
diff --git a/src/scrobbler/librefmscrobbler.h b/src/scrobbler/librefmscrobbler.h
deleted file mode 100644
index 82a6b4b79..000000000
--- a/src/scrobbler/librefmscrobbler.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Strawberry Music Player
- * Copyright 2018-2025, Jonas Kvinge
- *
- * 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 .
- *
- */
-
-#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 settings, const SharedPtr 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
diff --git a/src/scrobbler/scrobblingapi20.cpp b/src/scrobbler/scrobblingapi20.cpp
deleted file mode 100644
index 04f186a18..000000000
--- a/src/scrobbler/scrobblingapi20.cpp
+++ /dev/null
@@ -1,990 +0,0 @@
-/*
- * Strawberry Music Player
- * Copyright 2018-2025, Jonas Kvinge
- *
- * 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 .
- *
- */
-
-#include "config.h"
-
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#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 settings, const SharedPtr 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("
%1
").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(":
%1").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(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(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(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);
- }
-
-}
diff --git a/src/scrobbler/scrobblingapi20.h b/src/scrobbler/scrobblingapi20.h
deleted file mode 100644
index 46f6157c9..000000000
--- a/src/scrobbler/scrobblingapi20.h
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Strawberry Music Player
- * Copyright 2018-2025, Jonas Kvinge
- *
- * 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 .
- *
- */
-
-#ifndef SCROBBLINGAPI20_H
-#define SCROBBLINGAPI20_H
-
-#include "config.h"
-
-#include
-#include
-#include
-
-#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 settings, const SharedPtr 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 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
diff --git a/src/settings/scrobblersettingspage.cpp b/src/settings/scrobblersettingspage.cpp
index 0e09a8b92..c133dd859 100644
--- a/src/settings/scrobblersettingspage.cpp
+++ b/src/settings/scrobblersettingspage.cpp
@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
- * Copyright 2018-2021, Jonas Kvinge
+ * Copyright 2018-2025, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,7 +38,6 @@
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmscrobbler.h"
-#include "scrobbler/librefmscrobbler.h"
#include "scrobbler/listenbrainzscrobbler.h"
#include "constants/scrobblersettings.h"
@@ -50,10 +49,8 @@ ScrobblerSettingsPage::ScrobblerSettingsPage(SettingsDialog *dialog, const Share
ui_(new Ui_ScrobblerSettingsPage),
scrobbler_(scrobbler),
lastfmscrobbler_(scrobbler_->Service()),
- librefmscrobbler_(scrobbler_->Service()),
listenbrainzscrobbler_(scrobbler_->Service()),
lastfm_waiting_for_auth_(false),
- librefm_waiting_for_auth_(false),
listenbrainz_waiting_for_auth_(false) {
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);
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
QObject::connect(&*listenbrainzscrobbler_, &ListenBrainzScrobbler::AuthenticationComplete, this, &ScrobblerSettingsPage::ListenBrainz_AuthenticationComplete);
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());
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
- ui_->checkbox_librefm_enable->setChecked(librefmscrobbler_->enabled());
- LibreFM_RefreshControls(librefmscrobbler_->authenticated());
-
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
ui_->lineedit_listenbrainz_user_token->setText(listenbrainzscrobbler_->user_token());
ListenBrainz_RefreshControls(listenbrainzscrobbler_->authenticated());
@@ -167,10 +154,6 @@ void ScrobblerSettingsPage::Save() {
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
s.endGroup();
- s.beginGroup(LibreFMScrobbler::kSettingsGroup);
- s.setValue(kEnabled, ui_->checkbox_librefm_enable->isChecked());
- s.endGroup();
-
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
s.setValue(kEnabled, ui_->checkbox_listenbrainz_enable->isChecked());
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());
}
-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() {
listenbrainz_waiting_for_auth_ = true;
diff --git a/src/settings/scrobblersettingspage.h b/src/settings/scrobblersettingspage.h
index 29f9f3791..51a526f5e 100644
--- a/src/settings/scrobblersettingspage.h
+++ b/src/settings/scrobblersettingspage.h
@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
- * Copyright 2018-2021, Jonas Kvinge
+ * Copyright 2018-2025, Jonas Kvinge
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,7 +33,6 @@ class SettingsDialog;
class Ui_ScrobblerSettingsPage;
class AudioScrobbler;
class LastFMScrobbler;
-class LibreFMScrobbler;
class ListenBrainzScrobbler;
class ScrobblerSettingsPage : public SettingsPage {
@@ -50,9 +49,6 @@ class ScrobblerSettingsPage : public SettingsPage {
void LastFM_Login();
void LastFM_Logout();
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_Logout();
void ListenBrainz_AuthenticationComplete(const bool success, const QString &error = QString());
@@ -62,15 +58,12 @@ class ScrobblerSettingsPage : public SettingsPage {
const SharedPtr scrobbler_;
const SharedPtr lastfmscrobbler_;
- const SharedPtr librefmscrobbler_;
const SharedPtr listenbrainzscrobbler_;
bool lastfm_waiting_for_auth_;
- bool librefm_waiting_for_auth_;
bool listenbrainz_waiting_for_auth_;
void LastFM_RefreshControls(const bool authenticated);
- void LibreFM_RefreshControls(const bool authenticated);
void ListenBrainz_RefreshControls(const bool authenticated);
};
diff --git a/src/settings/scrobblersettingspage.ui b/src/settings/scrobblersettingspage.ui
index c7a5130cc..0b0606c4a 100644
--- a/src/settings/scrobblersettingspage.ui
+++ b/src/settings/scrobblersettingspage.ui
@@ -270,55 +270,6 @@
- -
-
-
- Libre.fm
-
-
-
-
-
-
- Enable
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
- Login
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -444,8 +395,6 @@
checkbox_source_radioparadise
checkbox_lastfm_enable
button_lastfm_login
- checkbox_librefm_enable
- button_librefm_login
checkbox_listenbrainz_enable
lineedit_listenbrainz_user_token
button_listenbrainz_login