fix(musicbrainz): query MusicBrainz directly per-IP instead of via the proxy
The Cloudflare proxy funnelled every user's MusicBrainz traffic onto a few egress IPs, and MusicBrainz rate-limits 1 req/s per IP, so the budget was shared across the whole userbase and produced 503s that worsen with growth. MB read access needs no key and authentication does not raise the limit, so the proxy added only the funnel; going per-IP makes the budget per-user. - qbz-integrations MusicBrainzConfig default is now use_proxy=false (direct). - client: handle HTTP 503 + Retry-After (not just 429, which the proxy used to translate); add parse_retry_after; route search_recording_by_isrc and search_artist through handle_response_status. - src-tauri session_lifecycle: MusicBrainz V2 init/teardown pass use_proxy=false. Slint inherits this via CoreBridge's default-constructed client. cc
This commit is contained in:
2 files changed
+34
-32
No files matched your search
@@ -72,7 +72,12 @@ impl Default for MusicBrainzConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
use_proxy: true,
|
||||
// Direct-to-MusicBrainz by default: each client uses its OWN IP, so
|
||||
// the per-IP 1 req/s budget is per-user instead of shared across all
|
||||
// QBZ users behind the proxy's Cloudflare egress IPs (which is what
|
||||
// triggered the 503s). MB read access needs no key, so the proxy
|
||||
// added only the funnel. Flip to true to route via the proxy again.
|
||||
use_proxy: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,19 +165,7 @@ impl MusicBrainzClient {
|
||||
let url = format!("{}/recording?query=isrc:{}&fmt=json", base, isrc);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
if status.as_u16() == 429 {
|
||||
return Err(IntegrationError::RateLimited(60));
|
||||
}
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(IntegrationError::internal(format!(
|
||||
"MusicBrainz search failed: {} - {}",
|
||||
status, text
|
||||
)));
|
||||
}
|
||||
|
||||
let response = self.handle_response_status(response).await?;
|
||||
response.json().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -198,19 +191,7 @@ impl MusicBrainzClient {
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
if status.as_u16() == 429 {
|
||||
return Err(IntegrationError::RateLimited(60));
|
||||
}
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
return Err(IntegrationError::internal(format!(
|
||||
"MusicBrainz artist search failed: {} - {}",
|
||||
status, text
|
||||
)));
|
||||
}
|
||||
|
||||
let response = self.handle_response_status(response).await?;
|
||||
response.json().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -757,8 +738,13 @@ impl MusicBrainzClient {
|
||||
return Ok(response);
|
||||
}
|
||||
let status = response.status();
|
||||
if status.as_u16() == 429 {
|
||||
return Err(IntegrationError::RateLimited(60));
|
||||
// 429 (proxy-translated) and 503 (direct MusicBrainz) both signal that
|
||||
// the rate limit was hit. Surface the server's Retry-After so the caller
|
||||
// can back off instead of treating it as a generic error.
|
||||
if matches!(status.as_u16(), 429 | 503) {
|
||||
return Err(IntegrationError::RateLimited(Self::parse_retry_after(
|
||||
&response,
|
||||
)));
|
||||
}
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(IntegrationError::internal(format!(
|
||||
@@ -767,6 +753,19 @@ impl MusicBrainzClient {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse the `Retry-After` header (whole seconds). MusicBrainz sends it on
|
||||
/// HTTP 503 when the per-IP rate limit is exceeded. Defaults to 1s because
|
||||
/// MB's per-IP limiter recovers within ~1 second.
|
||||
fn parse_retry_after(response: &reqwest::Response) -> u64 {
|
||||
response
|
||||
.headers()
|
||||
.get(reqwest::header::RETRY_AFTER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.filter(|&s| s > 0)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Escape special characters in Lucene queries
|
||||
fn escape_query(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
|
||||
Reference in new issue
Block a user