From 8a7a22e9bd96febe5b9f71b1067a6516851564ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:12:16 +0000 Subject: [PATCH 1/5] Initial plan From 4c8103ef6dc1d3d606cdf7d9691d9733cfc447db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:24:41 +0000 Subject: [PATCH 2/5] 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> --- src/constants/scrobblersettings.h | 1 + src/scrobbler/lastfmimport.cpp | 42 +++++++++++++++++++++++--- src/scrobbler/lastfmimport.h | 11 ++++--- src/scrobbler/lastfmscrobbler.cpp | 1 + src/scrobbler/lastfmscrobbler.h | 2 ++ src/settings/scrobblersettingspage.cpp | 2 ++ src/settings/scrobblersettingspage.ui | 38 +++++++++++++++++++++++ 7 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/constants/scrobblersettings.h b/src/constants/scrobblersettings.h index 6e6a45abe8..e0367b10ae 100644 --- a/src/constants/scrobblersettings.h +++ b/src/constants/scrobblersettings.h @@ -34,6 +34,7 @@ constexpr char kShowErrorDialog[] = "show_error_dialog"; constexpr char kStripRemastered[] = "strip_remastered"; constexpr char kSources[] = "sources"; constexpr char kUserToken[] = "user_token"; +constexpr char kApiKey[] = "api_key"; } // namespace ScrobblerSettings diff --git a/src/scrobbler/lastfmimport.cpp b/src/scrobbler/lastfmimport.cpp index d680a36ae9..137c9735bb 100644 --- a/src/scrobbler/lastfmimport.cpp +++ b/src/scrobbler/lastfmimport.cpp @@ -42,6 +42,7 @@ #include "core/logging.h" #include "core/networkaccessmanager.h" #include "core/settings.h" +#include "constants/scrobblersettings.h" #include "lastfmimport.h" @@ -51,6 +52,8 @@ using namespace Qt::Literals::StringLiterals; namespace { constexpr int kRequestsDelay = 2000; +constexpr int kMaxRetries = 5; +constexpr int kInitialBackoffMs = 5000; } LastFMImport::LastFMImport(const SharedPtr network, QObject *parent) @@ -101,14 +104,17 @@ void LastFMImport::ReloadSettings() { Settings s; s.beginGroup(LastFMScrobbler::kSettingsGroup); username_ = s.value("username").toString(); + api_key_ = s.value(ScrobblerSettings::kApiKey).toString(); s.endGroup(); } QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) { + const QString api_key = !api_key_.isEmpty() ? api_key_ : QLatin1String(LastFMScrobbler::kApiKey); + 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"lang"_s, QLocale().name().left(2).toLower()) << Param(u"format"_s, u"json"_s) @@ -234,11 +240,11 @@ void LastFMImport::SendGetRecentTracksRequest(GetRecentTracksRequest request) { } 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; replies_.removeAll(reply); @@ -247,10 +253,23 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in const JsonObjectResult json_object_result = ParseJsonObject(reply); 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); return; } + const int page = request.page; + QJsonObject json_object = json_object_result.json_object; if (json_object.isEmpty()) { return; @@ -390,11 +409,11 @@ void LastFMImport::SendGetTopTracksRequest(GetTopTracksRequest request) { } 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; replies_.removeAll(reply); @@ -403,10 +422,23 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p const JsonObjectResult json_object_result = ParseJsonObject(reply); 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); return; } + const int page = request.page; + QJsonObject json_object = json_object_result.json_object; if (json_object.isEmpty()) { return; diff --git a/src/scrobbler/lastfmimport.h b/src/scrobbler/lastfmimport.h index c4e54ead5d..ff05e3e1c6 100644 --- a/src/scrobbler/lastfmimport.h +++ b/src/scrobbler/lastfmimport.h @@ -60,12 +60,14 @@ class LastFMImport : public JsonBaseRequest { using ParamList = QList; 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 retry_count; }; 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 retry_count; }; private: @@ -95,14 +97,15 @@ class LastFMImport : public JsonBaseRequest { private Q_SLOTS: void FlushRequests(); - void GetRecentTracksRequestFinished(QNetworkReply *reply, const int page); - void GetTopTracksRequestFinished(QNetworkReply *reply, const int page); + void GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request); + void GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request); private: SharedPtr network_; QTimer *timer_flush_requests_; QString username_; + QString api_key_; bool lastplayed_; bool playcount_; int playcount_total_; diff --git a/src/scrobbler/lastfmscrobbler.cpp b/src/scrobbler/lastfmscrobbler.cpp index 0cb2a6011c..fd75c4a70e 100644 --- a/src/scrobbler/lastfmscrobbler.cpp +++ b/src/scrobbler/lastfmscrobbler.cpp @@ -113,6 +113,7 @@ void LastFMScrobbler::ReloadSettings() { s.beginGroup(kSettingsGroup); enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool(); + api_key_ = s.value(ScrobblerSettings::kApiKey).toString(); s.endGroup(); s.beginGroup(ScrobblerSettings::kSettingsGroup); diff --git a/src/scrobbler/lastfmscrobbler.h b/src/scrobbler/lastfmscrobbler.h index c8cd5b8fd4..4ca4dbb494 100644 --- a/src/scrobbler/lastfmscrobbler.h +++ b/src/scrobbler/lastfmscrobbler.h @@ -64,6 +64,7 @@ class LastFMScrobbler : public ScrobblerService { bool subscriber() const { return subscriber_; } bool submitted() const override { return submitted_; } QString username() const { return username_; } + QString api_key() const { return api_key_; } void Authenticate(); void UpdateNowPlaying(const Song &song) override; @@ -139,6 +140,7 @@ class LastFMScrobbler : public ScrobblerService { bool subscriber_; QString username_; QString session_key_; + QString api_key_; bool submitted_; Song song_playing_; diff --git a/src/settings/scrobblersettingspage.cpp b/src/settings/scrobblersettingspage.cpp index c133dd859a..0dee87b64c 100644 --- a/src/settings/scrobblersettingspage.cpp +++ b/src/settings/scrobblersettingspage.cpp @@ -106,6 +106,7 @@ void ScrobblerSettingsPage::Load() { ui_->checkbox_source_unknown->setChecked(scrobbler_->sources().contains(Song::Source::Unknown)); ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled()); + ui_->lineedit_lastfm_api_key->setText(lastfmscrobbler_->api_key()); LastFM_RefreshControls(lastfmscrobbler_->authenticated()); ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled()); @@ -152,6 +153,7 @@ void ScrobblerSettingsPage::Save() { s.beginGroup(LastFMScrobbler::kSettingsGroup); s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked()); + s.setValue(kApiKey, ui_->lineedit_lastfm_api_key->text()); s.endGroup(); s.beginGroup(ListenBrainzScrobbler::kSettingsGroup); diff --git a/src/settings/scrobblersettingspage.ui b/src/settings/scrobblersettingspage.ui index 0b0606c4a4..31d95f6564 100644 --- a/src/settings/scrobblersettingspage.ui +++ b/src/settings/scrobblersettingspage.ui @@ -234,6 +234,43 @@ + + + + + + + 80 + 0 + + + + API key: + + + + + + + Optional - your own Last.fm API key + + + + + + + + + <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> + + + true + + + true + + + @@ -394,6 +431,7 @@ checkbox_source_somafm checkbox_source_radioparadise checkbox_lastfm_enable + lineedit_lastfm_api_key button_lastfm_login checkbox_listenbrainz_enable lineedit_listenbrainz_user_token From 8bdbeeb5a8e8772a73813f42dfd230e6b2940297 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:27:23 +0000 Subject: [PATCH 3/5] Refactor retry logic to reduce code duplication - Extract retry condition check into ShouldRetryRequest() helper - Extract backoff delay calculation into CalculateBackoffDelay() helper - Use helper methods in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished - Improves code maintainability and consistency Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com> --- src/scrobbler/lastfmimport.cpp | 45 +++++++++++++++++++--------------- src/scrobbler/lastfmimport.h | 3 +++ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/scrobbler/lastfmimport.cpp b/src/scrobbler/lastfmimport.cpp index 137c9735bb..02e305b018 100644 --- a/src/scrobbler/lastfmimport.cpp +++ b/src/scrobbler/lastfmimport.cpp @@ -49,6 +49,7 @@ #include "lastfmscrobbler.h" using namespace Qt::Literals::StringLiterals; +using namespace ScrobblerSettings; namespace { constexpr int kRequestsDelay = 2000; @@ -253,16 +254,14 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecen const JsonObjectResult json_object_result = ParseJsonObject(reply); 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; - } + if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) { + const int delay_ms = CalculateBackoffDelay(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); return; @@ -422,16 +421,14 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTrack const JsonObjectResult json_object_result = ParseJsonObject(reply); 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; - } + if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) { + const int delay_ms = CalculateBackoffDelay(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); return; @@ -548,6 +545,14 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTrack } +bool LastFMImport::ShouldRetryRequest(const JsonObjectResult &result) const { + return result.http_status_code == 500 || result.http_status_code == 503 || result.network_error == QNetworkReply::TemporaryNetworkFailureError; +} + +int LastFMImport::CalculateBackoffDelay(const int retry_count) const { + return kInitialBackoffMs * (1 << retry_count); +} + void LastFMImport::UpdateTotalCheck() { Q_EMIT UpdateTotal(lastplayed_total_, playcount_total_); diff --git a/src/scrobbler/lastfmimport.h b/src/scrobbler/lastfmimport.h index ff05e3e1c6..bf3c5c0741 100644 --- a/src/scrobbler/lastfmimport.h +++ b/src/scrobbler/lastfmimport.h @@ -80,6 +80,9 @@ class LastFMImport : public JsonBaseRequest { void SendGetRecentTracksRequest(GetRecentTracksRequest request); void SendGetTopTracksRequest(GetTopTracksRequest request); + bool ShouldRetryRequest(const JsonObjectResult &result) const; + int CalculateBackoffDelay(const int retry_count) const; + void Error(const QString &error, const QVariant &debug = QVariant()) override; void UpdateTotalCheck(); From c25d8a5e6c3928a33d126852aaa0ee145cc72454 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:29:11 +0000 Subject: [PATCH 4/5] Improve retry logic safety and readability - Add named constants for retry-eligible HTTP status codes (500, 503) - Add bounds checking in backoff calculation to prevent integer overflow - Use kMaxBackoffShift constant to limit bit shift operations - Improves code safety and readability Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com> --- src/scrobbler/lastfmimport.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/scrobbler/lastfmimport.cpp b/src/scrobbler/lastfmimport.cpp index 02e305b018..10a5624892 100644 --- a/src/scrobbler/lastfmimport.cpp +++ b/src/scrobbler/lastfmimport.cpp @@ -55,6 +55,9 @@ namespace { constexpr int kRequestsDelay = 2000; constexpr int kMaxRetries = 5; constexpr int kInitialBackoffMs = 5000; +constexpr int kMaxBackoffShift = 10; // Maximum shift value to prevent overflow +constexpr int kRetryHttpStatusCode1 = 500; // Internal Server Error +constexpr int kRetryHttpStatusCode2 = 503; // Service Unavailable } LastFMImport::LastFMImport(const SharedPtr network, QObject *parent) @@ -546,11 +549,14 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTrack } bool LastFMImport::ShouldRetryRequest(const JsonObjectResult &result) const { - return result.http_status_code == 500 || result.http_status_code == 503 || result.network_error == QNetworkReply::TemporaryNetworkFailureError; + return result.http_status_code == kRetryHttpStatusCode1 || + result.http_status_code == kRetryHttpStatusCode2 || + result.network_error == QNetworkReply::TemporaryNetworkFailureError; } int LastFMImport::CalculateBackoffDelay(const int retry_count) const { - return kInitialBackoffMs * (1 << retry_count); + const int safe_shift = std::min(retry_count, kMaxBackoffShift); + return kInitialBackoffMs * (1 << safe_shift); } void LastFMImport::UpdateTotalCheck() { From 07900f126569df51e92fec1e2cd5e410f996b116 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:30:38 +0000 Subject: [PATCH 5/5] Extract retry logging into helper method and fix formatting - Add LogRetryAttempt() helper method for consistent logging - Fix formatting in ShouldRetryRequest() for better readability - Use helper method in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished - Eliminates duplicate logging code Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com> --- src/scrobbler/lastfmimport.cpp | 14 ++++++++++---- src/scrobbler/lastfmimport.h | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/scrobbler/lastfmimport.cpp b/src/scrobbler/lastfmimport.cpp index 10a5624892..cca7e8eb94 100644 --- a/src/scrobbler/lastfmimport.cpp +++ b/src/scrobbler/lastfmimport.cpp @@ -259,7 +259,7 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecen if (!json_object_result.success()) { if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) { const int delay_ms = CalculateBackoffDelay(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 << ")"; + LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms); QTimer::singleShot(delay_ms, this, [this, request]() { GetRecentTracksRequest retry_request(request.page, request.retry_count + 1); SendGetRecentTracksRequest(retry_request); @@ -426,7 +426,7 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTrack if (!json_object_result.success()) { if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) { const int delay_ms = CalculateBackoffDelay(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 << ")"; + LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms); QTimer::singleShot(delay_ms, this, [this, request]() { GetTopTracksRequest retry_request(request.page, request.retry_count + 1); SendGetTopTracksRequest(retry_request); @@ -549,11 +549,17 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTrack } bool LastFMImport::ShouldRetryRequest(const JsonObjectResult &result) const { - return result.http_status_code == kRetryHttpStatusCode1 || - result.http_status_code == kRetryHttpStatusCode2 || + return result.http_status_code == kRetryHttpStatusCode1 || + result.http_status_code == kRetryHttpStatusCode2 || result.network_error == QNetworkReply::TemporaryNetworkFailureError; } +void LastFMImport::LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const { + qLog(Warning) << "Last.fm request failed with status" << http_status_code + << ", retrying in" << delay_ms << "ms (attempt" + << (retry_count + 1) << "of" << kMaxRetries << ")"; +} + int LastFMImport::CalculateBackoffDelay(const int retry_count) const { const int safe_shift = std::min(retry_count, kMaxBackoffShift); return kInitialBackoffMs * (1 << safe_shift); diff --git a/src/scrobbler/lastfmimport.h b/src/scrobbler/lastfmimport.h index bf3c5c0741..15a6c60712 100644 --- a/src/scrobbler/lastfmimport.h +++ b/src/scrobbler/lastfmimport.h @@ -82,6 +82,7 @@ class LastFMImport : public JsonBaseRequest { bool ShouldRetryRequest(const JsonObjectResult &result) const; int CalculateBackoffDelay(const int retry_count) const; + void LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const; void Error(const QString &error, const QVariant &debug = QVariant()) override;