Add custom API key support and retry logic for Last.fm import
- Add API key field to Last.fm settings UI with helpful info text - Store and load custom API key from settings - Use custom API key in lastfmimport if provided, fall back to default - Implement exponential backoff retry logic (up to 5 retries) - Retry on HTTP 500/503 errors with increasing delays (5s, 10s, 20s, 40s, 80s) - Add retry count tracking to request structures Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,7 @@ constexpr char kShowErrorDialog[] = "show_error_dialog";
|
|||||||
constexpr char kStripRemastered[] = "strip_remastered";
|
constexpr char kStripRemastered[] = "strip_remastered";
|
||||||
constexpr char kSources[] = "sources";
|
constexpr char kSources[] = "sources";
|
||||||
constexpr char kUserToken[] = "user_token";
|
constexpr char kUserToken[] = "user_token";
|
||||||
|
constexpr char kApiKey[] = "api_key";
|
||||||
|
|
||||||
} // namespace ScrobblerSettings
|
} // namespace ScrobblerSettings
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/networkaccessmanager.h"
|
#include "core/networkaccessmanager.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
|
#include "constants/scrobblersettings.h"
|
||||||
|
|
||||||
#include "lastfmimport.h"
|
#include "lastfmimport.h"
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ using namespace Qt::Literals::StringLiterals;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kRequestsDelay = 2000;
|
constexpr int kRequestsDelay = 2000;
|
||||||
|
constexpr int kMaxRetries = 5;
|
||||||
|
constexpr int kInitialBackoffMs = 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||||
@@ -101,14 +104,17 @@ void LastFMImport::ReloadSettings() {
|
|||||||
Settings s;
|
Settings s;
|
||||||
s.beginGroup(LastFMScrobbler::kSettingsGroup);
|
s.beginGroup(LastFMScrobbler::kSettingsGroup);
|
||||||
username_ = s.value("username").toString();
|
username_ = s.value("username").toString();
|
||||||
|
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
|
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
|
||||||
|
|
||||||
|
const QString api_key = !api_key_.isEmpty() ? api_key_ : QLatin1String(LastFMScrobbler::kApiKey);
|
||||||
|
|
||||||
ParamList params = ParamList()
|
ParamList params = ParamList()
|
||||||
<< Param(u"api_key"_s, QLatin1String(LastFMScrobbler::kApiKey))
|
<< Param(u"api_key"_s, api_key)
|
||||||
<< Param(u"user"_s, username_)
|
<< Param(u"user"_s, username_)
|
||||||
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
|
||||||
<< Param(u"format"_s, u"json"_s)
|
<< Param(u"format"_s, u"json"_s)
|
||||||
@@ -234,11 +240,11 @@ void LastFMImport::SendGetRecentTracksRequest(GetRecentTracksRequest request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request.page); });
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request); });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const int page) {
|
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request) {
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
if (!replies_.contains(reply)) return;
|
||||||
replies_.removeAll(reply);
|
replies_.removeAll(reply);
|
||||||
@@ -247,10 +253,23 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
|
|||||||
|
|
||||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
if (!json_object_result.success()) {
|
if (!json_object_result.success()) {
|
||||||
|
if (json_object_result.http_status_code == 500 || json_object_result.http_status_code == 503 || json_object_result.network_error == QNetworkReply::TemporaryNetworkFailureError) {
|
||||||
|
if (request.retry_count < kMaxRetries) {
|
||||||
|
const int delay_ms = kInitialBackoffMs * (1 << request.retry_count);
|
||||||
|
qLog(Warning) << "Last.fm request failed with status" << json_object_result.http_status_code << ", retrying in" << delay_ms << "ms (attempt" << (request.retry_count + 1) << "of" << kMaxRetries << ")";
|
||||||
|
QTimer::singleShot(delay_ms, this, [this, request]() {
|
||||||
|
GetRecentTracksRequest retry_request(request.page, request.retry_count + 1);
|
||||||
|
SendGetRecentTracksRequest(retry_request);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
Error(json_object_result.error_message);
|
Error(json_object_result.error_message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int page = request.page;
|
||||||
|
|
||||||
QJsonObject json_object = json_object_result.json_object;
|
QJsonObject json_object = json_object_result.json_object;
|
||||||
if (json_object.isEmpty()) {
|
if (json_object.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
@@ -390,11 +409,11 @@ void LastFMImport::SendGetTopTracksRequest(GetTopTracksRequest request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = CreateRequest(params);
|
QNetworkReply *reply = CreateRequest(params);
|
||||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request.page); });
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request); });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int page) {
|
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request) {
|
||||||
|
|
||||||
if (!replies_.contains(reply)) return;
|
if (!replies_.contains(reply)) return;
|
||||||
replies_.removeAll(reply);
|
replies_.removeAll(reply);
|
||||||
@@ -403,10 +422,23 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
|
|||||||
|
|
||||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
if (!json_object_result.success()) {
|
if (!json_object_result.success()) {
|
||||||
|
if (json_object_result.http_status_code == 500 || json_object_result.http_status_code == 503 || json_object_result.network_error == QNetworkReply::TemporaryNetworkFailureError) {
|
||||||
|
if (request.retry_count < kMaxRetries) {
|
||||||
|
const int delay_ms = kInitialBackoffMs * (1 << request.retry_count);
|
||||||
|
qLog(Warning) << "Last.fm request failed with status" << json_object_result.http_status_code << ", retrying in" << delay_ms << "ms (attempt" << (request.retry_count + 1) << "of" << kMaxRetries << ")";
|
||||||
|
QTimer::singleShot(delay_ms, this, [this, request]() {
|
||||||
|
GetTopTracksRequest retry_request(request.page, request.retry_count + 1);
|
||||||
|
SendGetTopTracksRequest(retry_request);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
Error(json_object_result.error_message);
|
Error(json_object_result.error_message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int page = request.page;
|
||||||
|
|
||||||
QJsonObject json_object = json_object_result.json_object;
|
QJsonObject json_object = json_object_result.json_object;
|
||||||
if (json_object.isEmpty()) {
|
if (json_object.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ class LastFMImport : public JsonBaseRequest {
|
|||||||
using ParamList = QList<Param>;
|
using ParamList = QList<Param>;
|
||||||
|
|
||||||
struct GetRecentTracksRequest {
|
struct GetRecentTracksRequest {
|
||||||
explicit GetRecentTracksRequest(const int _page) : page(_page) {}
|
explicit GetRecentTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
|
||||||
int page;
|
int page;
|
||||||
|
int retry_count;
|
||||||
};
|
};
|
||||||
struct GetTopTracksRequest {
|
struct GetTopTracksRequest {
|
||||||
explicit GetTopTracksRequest(const int _page) : page(_page) {}
|
explicit GetTopTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
|
||||||
int page;
|
int page;
|
||||||
|
int retry_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -95,14 +97,15 @@ class LastFMImport : public JsonBaseRequest {
|
|||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void FlushRequests();
|
void FlushRequests();
|
||||||
void GetRecentTracksRequestFinished(QNetworkReply *reply, const int page);
|
void GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request);
|
||||||
void GetTopTracksRequestFinished(QNetworkReply *reply, const int page);
|
void GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SharedPtr<NetworkAccessManager> network_;
|
SharedPtr<NetworkAccessManager> network_;
|
||||||
QTimer *timer_flush_requests_;
|
QTimer *timer_flush_requests_;
|
||||||
|
|
||||||
QString username_;
|
QString username_;
|
||||||
|
QString api_key_;
|
||||||
bool lastplayed_;
|
bool lastplayed_;
|
||||||
bool playcount_;
|
bool playcount_;
|
||||||
int playcount_total_;
|
int playcount_total_;
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ void LastFMScrobbler::ReloadSettings() {
|
|||||||
|
|
||||||
s.beginGroup(kSettingsGroup);
|
s.beginGroup(kSettingsGroup);
|
||||||
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
|
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
|
||||||
|
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
s.beginGroup(ScrobblerSettings::kSettingsGroup);
|
s.beginGroup(ScrobblerSettings::kSettingsGroup);
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class LastFMScrobbler : public ScrobblerService {
|
|||||||
bool subscriber() const { return subscriber_; }
|
bool subscriber() const { return subscriber_; }
|
||||||
bool submitted() const override { return submitted_; }
|
bool submitted() const override { return submitted_; }
|
||||||
QString username() const { return username_; }
|
QString username() const { return username_; }
|
||||||
|
QString api_key() const { return api_key_; }
|
||||||
|
|
||||||
void Authenticate();
|
void Authenticate();
|
||||||
void UpdateNowPlaying(const Song &song) override;
|
void UpdateNowPlaying(const Song &song) override;
|
||||||
@@ -139,6 +140,7 @@ class LastFMScrobbler : public ScrobblerService {
|
|||||||
bool subscriber_;
|
bool subscriber_;
|
||||||
QString username_;
|
QString username_;
|
||||||
QString session_key_;
|
QString session_key_;
|
||||||
|
QString api_key_;
|
||||||
|
|
||||||
bool submitted_;
|
bool submitted_;
|
||||||
Song song_playing_;
|
Song song_playing_;
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ void ScrobblerSettingsPage::Load() {
|
|||||||
ui_->checkbox_source_unknown->setChecked(scrobbler_->sources().contains(Song::Source::Unknown));
|
ui_->checkbox_source_unknown->setChecked(scrobbler_->sources().contains(Song::Source::Unknown));
|
||||||
|
|
||||||
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
|
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
|
||||||
|
ui_->lineedit_lastfm_api_key->setText(lastfmscrobbler_->api_key());
|
||||||
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
|
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
|
||||||
|
|
||||||
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
|
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
|
||||||
@@ -152,6 +153,7 @@ void ScrobblerSettingsPage::Save() {
|
|||||||
|
|
||||||
s.beginGroup(LastFMScrobbler::kSettingsGroup);
|
s.beginGroup(LastFMScrobbler::kSettingsGroup);
|
||||||
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
|
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
|
||||||
|
s.setValue(kApiKey, ui_->lineedit_lastfm_api_key->text());
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
|
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);
|
||||||
|
|||||||
@@ -234,6 +234,43 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="layout_lastfm_api_key">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_lastfm_api_key">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>API key:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="lineedit_lastfm_api_key">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Optional - your own Last.fm API key</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_lastfm_api_key_info">
|
||||||
|
<property name="text">
|
||||||
|
<string><html><head/><body><p><span style=" font-size:8pt;">Using your own API key can help avoid rate limiting for large libraries. Get one at </span><a href="https://www.last.fm/api/account/create"><span style=" font-size:8pt; text-decoration: underline; color:#0000ff;">https://www.last.fm/api/account/create</span></a></p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
|
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
|
||||||
</item>
|
</item>
|
||||||
@@ -394,6 +431,7 @@
|
|||||||
<tabstop>checkbox_source_somafm</tabstop>
|
<tabstop>checkbox_source_somafm</tabstop>
|
||||||
<tabstop>checkbox_source_radioparadise</tabstop>
|
<tabstop>checkbox_source_radioparadise</tabstop>
|
||||||
<tabstop>checkbox_lastfm_enable</tabstop>
|
<tabstop>checkbox_lastfm_enable</tabstop>
|
||||||
|
<tabstop>lineedit_lastfm_api_key</tabstop>
|
||||||
<tabstop>button_lastfm_login</tabstop>
|
<tabstop>button_lastfm_login</tabstop>
|
||||||
<tabstop>checkbox_listenbrainz_enable</tabstop>
|
<tabstop>checkbox_listenbrainz_enable</tabstop>
|
||||||
<tabstop>lineedit_listenbrainz_user_token</tabstop>
|
<tabstop>lineedit_listenbrainz_user_token</tabstop>
|
||||||
|
|||||||
Reference in New Issue
Block a user