From 4b014253cfdc1047a7d86d415c08cc88d891724a Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Thu, 18 Sep 2025 00:22:11 +0200 Subject: [PATCH] Remove libre.fm --- CMakeLists.txt | 4 - README.md | 2 +- debian/control | 2 +- ...rawberrymusicplayer.strawberry.appdata.xml | 2 +- dist/unix/strawberry.1 | 2 +- dist/unix/strawberry.spec.in | 2 +- src/core/application.cpp | 2 - src/scrobbler/lastfmimport.cpp | 3 +- src/scrobbler/lastfmscrobbler.cpp | 950 ++++++++++++++++- src/scrobbler/lastfmscrobbler.h | 113 +- src/scrobbler/librefmscrobbler.cpp | 36 - src/scrobbler/librefmscrobbler.h | 46 - src/scrobbler/scrobblingapi20.cpp | 990 ------------------ src/scrobbler/scrobblingapi20.h | 155 --- src/settings/scrobblersettingspage.cpp | 54 +- src/settings/scrobblersettingspage.h | 9 +- src/settings/scrobblersettingspage.ui | 51 - 17 files changed, 1068 insertions(+), 1355 deletions(-) delete mode 100644 src/scrobbler/librefmscrobbler.cpp delete mode 100644 src/scrobbler/librefmscrobbler.h delete mode 100644 src/scrobbler/scrobblingapi20.cpp delete mode 100644 src/scrobbler/scrobblingapi20.h 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