chore(qconnect): checkpoint current poc state
cx
This commit is contained in:
92 files changed
+3886
-2433
No files matched your search
@@ -3,8 +3,11 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
|
||||
use super::models::{
|
||||
AuthGetSessionResponse, AuthGetTokenResponse, LastFmResponse, LastFmSession,
|
||||
LastFmSimilarArtist,
|
||||
};
|
||||
use crate::error::{IntegrationError, IntegrationResult};
|
||||
use super::models::{AuthGetSessionResponse, AuthGetTokenResponse, LastFmResponse, LastFmSession, LastFmSimilarArtist};
|
||||
|
||||
/// Cloudflare Workers proxy URL - handles API credentials and signature generation
|
||||
const LASTFM_PROXY_URL: &str = "https://qbz-api-proxy.blitzkriegfc.workers.dev/lastfm";
|
||||
@@ -78,25 +81,18 @@ impl LastFmClient {
|
||||
pub async fn get_token(&self) -> IntegrationResult<(String, String)> {
|
||||
let url = format!("{}/auth.getToken", LASTFM_PROXY_URL);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.json(&json!({}))
|
||||
.send()
|
||||
.await?;
|
||||
let response = self.client.post(&url).json(&json!({})).send().await?;
|
||||
|
||||
let data: LastFmResponse<AuthGetTokenResponse> = response.json().await?;
|
||||
|
||||
match data {
|
||||
LastFmResponse::Success(r) => {
|
||||
let auth_url = r.auth_url.unwrap_or_else(|| {
|
||||
format!("https://www.last.fm/api/auth/?token={}", r.token)
|
||||
});
|
||||
let auth_url = r
|
||||
.auth_url
|
||||
.unwrap_or_else(|| format!("https://www.last.fm/api/auth/?token={}", r.token));
|
||||
Ok((r.token, auth_url))
|
||||
}
|
||||
LastFmResponse::Error { error, message } => {
|
||||
Err(IntegrationError::api(error, message))
|
||||
}
|
||||
LastFmResponse::Error { error, message } => Err(IntegrationError::api(error, message)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +167,10 @@ impl LastFmClient {
|
||||
Ok(())
|
||||
} else {
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(IntegrationError::internal(format!("Scrobble failed: {}", text)))
|
||||
Err(IntegrationError::internal(format!(
|
||||
"Scrobble failed: {}",
|
||||
text
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,8 @@ impl ListenBrainzCache {
|
||||
isrc: Option<&str>,
|
||||
duration_ms: Option<u64>,
|
||||
) -> Result<i64, String> {
|
||||
let artist_mbids_json = artist_mbids.map(|ids| serde_json::to_string(ids).unwrap_or_default());
|
||||
let artist_mbids_json =
|
||||
artist_mbids.map(|ids| serde_json::to_string(ids).unwrap_or_default());
|
||||
|
||||
self.conn
|
||||
.execute(
|
||||
@@ -183,7 +184,8 @@ impl ListenBrainzCache {
|
||||
let listens = stmt
|
||||
.query_map([limit], |row| {
|
||||
let artist_mbids_json: Option<String> = row.get(7)?;
|
||||
let artist_mbids = artist_mbids_json.and_then(|json| serde_json::from_str(&json).ok());
|
||||
let artist_mbids =
|
||||
artist_mbids_json.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
Ok(QueuedListen {
|
||||
id: row.get(0)?,
|
||||
@@ -241,7 +243,8 @@ impl ListenBrainzCache {
|
||||
.iter()
|
||||
.map(|id| Box::new(*id) as Box<dyn rusqlite::types::ToSql>)
|
||||
.collect();
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
||||
params.iter().map(|p| p.as_ref()).collect();
|
||||
self.conn
|
||||
.execute(&sql, params_refs.as_slice())
|
||||
.map_err(|e| format!("Failed to batch mark sent: {}", e))?;
|
||||
@@ -252,7 +255,11 @@ impl ListenBrainzCache {
|
||||
pub fn get_queue_count(&self) -> Result<u32, String> {
|
||||
let count: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM listen_queue WHERE sent = 0", [], |row| row.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM listen_queue WHERE sent = 0",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
Ok(count as u32)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use reqwest::Client;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::error::{IntegrationError, IntegrationResult};
|
||||
use super::models::*;
|
||||
use crate::error::{IntegrationError, IntegrationResult};
|
||||
|
||||
/// ListenBrainz API base URL
|
||||
const LISTENBRAINZ_API_URL: &str = "https://api.listenbrainz.org/1";
|
||||
@@ -113,9 +113,9 @@ impl ListenBrainzClient {
|
||||
return Err(IntegrationError::AuthFailed(validation.message));
|
||||
}
|
||||
|
||||
let user_name = validation
|
||||
.user_name
|
||||
.ok_or_else(|| IntegrationError::AuthFailed("Token valid but no username returned".into()))?;
|
||||
let user_name = validation.user_name.ok_or_else(|| {
|
||||
IntegrationError::AuthFailed("Token valid but no username returned".into())
|
||||
})?;
|
||||
|
||||
// Store validated token and username
|
||||
{
|
||||
@@ -174,7 +174,10 @@ impl ListenBrainzClient {
|
||||
)));
|
||||
}
|
||||
|
||||
response.json::<TokenValidationResponse>().await.map_err(Into::into)
|
||||
response
|
||||
.json::<TokenValidationResponse>()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Submit "now playing" notification
|
||||
@@ -261,7 +264,11 @@ impl ListenBrainzClient {
|
||||
}
|
||||
|
||||
/// Internal: Submit listens to API
|
||||
async fn submit_listens(&self, token: &str, payload: &SubmitListensPayload) -> IntegrationResult<()> {
|
||||
async fn submit_listens(
|
||||
&self,
|
||||
token: &str,
|
||||
payload: &SubmitListensPayload,
|
||||
) -> IntegrationResult<()> {
|
||||
let url = format!("{}/submit-listens", LISTENBRAINZ_API_URL);
|
||||
|
||||
let response = self
|
||||
|
||||
@@ -45,6 +45,6 @@ pub mod cache;
|
||||
|
||||
pub use client::{ListenBrainzClient, ListenBrainzConfig};
|
||||
pub use models::{
|
||||
ListenType, TrackMetadata, AdditionalInfo, Listen, SubmitListensPayload,
|
||||
ListenBrainzStatus, UserInfo, QueuedListen,
|
||||
AdditionalInfo, Listen, ListenBrainzStatus, ListenType, QueuedListen, SubmitListensPayload,
|
||||
TrackMetadata, UserInfo,
|
||||
};
|
||||
@@ -8,8 +8,8 @@ use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::models::{
|
||||
ArtistMetadata, ArtistRelationships, LocationDiscoveryResponse,
|
||||
MatchConfidence, ResolvedArtist, ResolvedTrack, ArtistType,
|
||||
ArtistMetadata, ArtistRelationships, ArtistType, LocationDiscoveryResponse, MatchConfidence,
|
||||
ResolvedArtist, ResolvedTrack,
|
||||
};
|
||||
|
||||
/// TTL for recording cache (30 days)
|
||||
@@ -245,7 +245,10 @@ impl MusicBrainzCache {
|
||||
// ============ Artist Cache (JSON-serialized) ============
|
||||
|
||||
/// Get cached artist by name (JSON-serialized)
|
||||
pub fn get_artist_by_name<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<Option<T>, String> {
|
||||
pub fn get_artist_by_name<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<Option<T>, String> {
|
||||
let normalized = Self::normalize_name(name);
|
||||
let min_fetched_at = Self::current_timestamp() - ARTIST_TTL_SECS;
|
||||
let result: Option<String> = self
|
||||
@@ -268,7 +271,11 @@ impl MusicBrainzCache {
|
||||
}
|
||||
|
||||
/// Cache an artist (JSON-serialized)
|
||||
pub fn set_artist_by_name<T: serde::Serialize>(&self, name: &str, data: &T) -> Result<(), String> {
|
||||
pub fn set_artist_by_name<T: serde::Serialize>(
|
||||
&self,
|
||||
name: &str,
|
||||
data: &T,
|
||||
) -> Result<(), String> {
|
||||
let normalized = Self::normalize_name(name);
|
||||
let fetched_at = Self::current_timestamp();
|
||||
let json = serde_json::to_string(data)
|
||||
@@ -285,7 +292,10 @@ impl MusicBrainzCache {
|
||||
// ============ Release Cache ============
|
||||
|
||||
/// Get cached release by barcode
|
||||
pub fn get_release<T: serde::de::DeserializeOwned>(&self, barcode: &str) -> Result<Option<T>, String> {
|
||||
pub fn get_release<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
barcode: &str,
|
||||
) -> Result<Option<T>, String> {
|
||||
let min_fetched_at = Self::current_timestamp() - RELEASE_TTL_SECS;
|
||||
let result: Option<String> = self
|
||||
.conn
|
||||
@@ -345,7 +355,11 @@ impl MusicBrainzCache {
|
||||
}
|
||||
|
||||
/// Cache artist relationships
|
||||
pub fn set_artist_relations(&self, mbid: &str, data: &ArtistRelationships) -> Result<(), String> {
|
||||
pub fn set_artist_relations(
|
||||
&self,
|
||||
mbid: &str,
|
||||
data: &ArtistRelationships,
|
||||
) -> Result<(), String> {
|
||||
let fetched_at = Self::current_timestamp();
|
||||
let json = serde_json::to_string(data)
|
||||
.map_err(|e| format!("Failed to serialize relations: {}", e))?;
|
||||
@@ -399,7 +413,10 @@ impl MusicBrainzCache {
|
||||
// ============ Scene Discovery Cache ============
|
||||
|
||||
/// Get cached scene discovery results
|
||||
pub fn get_scene_cache(&self, cache_key: &str) -> Result<Option<LocationDiscoveryResponse>, String> {
|
||||
pub fn get_scene_cache(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
) -> Result<Option<LocationDiscoveryResponse>, String> {
|
||||
let min_fetched_at = Self::current_timestamp() - SCENE_TTL_SECS;
|
||||
let result: Option<String> = self
|
||||
.conn
|
||||
@@ -421,10 +438,14 @@ impl MusicBrainzCache {
|
||||
}
|
||||
|
||||
/// Cache scene discovery results
|
||||
pub fn set_scene_cache(&self, cache_key: &str, data: &LocationDiscoveryResponse) -> Result<(), String> {
|
||||
pub fn set_scene_cache(
|
||||
&self,
|
||||
cache_key: &str,
|
||||
data: &LocationDiscoveryResponse,
|
||||
) -> Result<(), String> {
|
||||
let fetched_at = Self::current_timestamp();
|
||||
let json = serde_json::to_string(data)
|
||||
.map_err(|e| format!("Failed to serialize scene: {}", e))?;
|
||||
let json =
|
||||
serde_json::to_string(data).map_err(|e| format!("Failed to serialize scene: {}", e))?;
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO mb_scene_cache (cache_key, data, fetched_at) VALUES (?, ?, ?)",
|
||||
@@ -621,7 +642,10 @@ impl MusicBrainzCache {
|
||||
}
|
||||
|
||||
if total_deleted > 0 {
|
||||
log::info!("MusicBrainz cache cleanup: removed {} expired entries", total_deleted);
|
||||
log::info!(
|
||||
"MusicBrainz cache cleanup: removed {} expired entries",
|
||||
total_deleted
|
||||
);
|
||||
}
|
||||
Ok(total_deleted)
|
||||
}
|
||||
@@ -650,17 +674,23 @@ impl MusicBrainzCache {
|
||||
|
||||
/// Get cache statistics
|
||||
pub fn get_stats(&self) -> Result<CacheStats, String> {
|
||||
let recordings: i64 = self.conn
|
||||
let recordings: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM mb_recordings", [], |row| row.get(0))
|
||||
.unwrap_or(0);
|
||||
let artists: i64 = self.conn
|
||||
let artists: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM mb_artists", [], |row| row.get(0))
|
||||
.unwrap_or(0);
|
||||
let releases: i64 = self.conn
|
||||
let releases: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM mb_releases", [], |row| row.get(0))
|
||||
.unwrap_or(0);
|
||||
let relations: i64 = self.conn
|
||||
.query_row("SELECT COUNT(*) FROM mb_artist_relations", [], |row| row.get(0))
|
||||
let relations: i64 = self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM mb_artist_relations", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(CacheStats {
|
||||
@@ -674,12 +704,14 @@ impl MusicBrainzCache {
|
||||
/// TTL-based cleanup (V2 style)
|
||||
pub fn cleanup(&self, ttl_days: u32) -> Result<(u64, u64), String> {
|
||||
let cutoff = chrono::Utc::now().timestamp() - (ttl_days as i64 * 86400);
|
||||
let tracks_deleted = self.conn
|
||||
.execute("DELETE FROM resolved_tracks WHERE cached_at < ?", [cutoff])
|
||||
.map_err(|e| format!("Failed to cleanup tracks: {}", e))? as u64;
|
||||
let artists_deleted = self.conn
|
||||
.execute("DELETE FROM resolved_artists WHERE cached_at < ?", [cutoff])
|
||||
.map_err(|e| format!("Failed to cleanup artists: {}", e))? as u64;
|
||||
let tracks_deleted =
|
||||
self.conn
|
||||
.execute("DELETE FROM resolved_tracks WHERE cached_at < ?", [cutoff])
|
||||
.map_err(|e| format!("Failed to cleanup tracks: {}", e))? as u64;
|
||||
let artists_deleted =
|
||||
self.conn
|
||||
.execute("DELETE FROM resolved_artists WHERE cached_at < ?", [cutoff])
|
||||
.map_err(|e| format!("Failed to cleanup artists: {}", e))? as u64;
|
||||
Ok((tracks_deleted, artists_deleted))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::error::{IntegrationError, IntegrationResult};
|
||||
use super::models::*;
|
||||
use crate::error::{IntegrationError, IntegrationResult};
|
||||
|
||||
/// Proxy URL for MusicBrainz requests
|
||||
const MUSICBRAINZ_PROXY_URL: &str = "https://qbz-api-proxy.blitzkriegfc.workers.dev/musicbrainz";
|
||||
@@ -144,7 +144,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Search recordings by ISRC
|
||||
pub async fn search_recording_by_isrc(&self, isrc: &str) -> IntegrationResult<RecordingSearchResponse> {
|
||||
pub async fn search_recording_by_isrc(
|
||||
&self,
|
||||
isrc: &str,
|
||||
) -> IntegrationResult<RecordingSearchResponse> {
|
||||
if !self.is_enabled().await {
|
||||
return Err(IntegrationError::ServiceUnavailable(
|
||||
"MusicBrainz integration is disabled".into(),
|
||||
@@ -174,7 +177,11 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Search artists by name
|
||||
pub async fn search_artist(&self, name: &str, limit: u32) -> IntegrationResult<ArtistSearchResponse> {
|
||||
pub async fn search_artist(
|
||||
&self,
|
||||
name: &str,
|
||||
limit: u32,
|
||||
) -> IntegrationResult<ArtistSearchResponse> {
|
||||
if !self.is_enabled().await {
|
||||
return Err(IntegrationError::ServiceUnavailable(
|
||||
"MusicBrainz integration is disabled".into(),
|
||||
@@ -220,7 +227,11 @@ impl MusicBrainzClient {
|
||||
if let Some(isrc) = isrc {
|
||||
let response = self.search_recording_by_isrc(isrc).await?;
|
||||
if let Some(recording) = response.recordings.first() {
|
||||
let confidence = if recording.isrcs.as_ref().map_or(false, |isrcs| isrcs.contains(&isrc.to_string())) {
|
||||
let confidence = if recording
|
||||
.isrcs
|
||||
.as_ref()
|
||||
.map_or(false, |isrcs| isrcs.contains(&isrc.to_string()))
|
||||
{
|
||||
MatchConfidence::Exact
|
||||
} else {
|
||||
MatchConfidence::from_score(recording.score)
|
||||
@@ -275,7 +286,11 @@ impl MusicBrainzClient {
|
||||
// ============ Extended API Methods ============
|
||||
|
||||
/// Search recordings by title and artist
|
||||
pub async fn search_recording(&self, title: &str, artist: &str) -> IntegrationResult<RecordingSearchResponse> {
|
||||
pub async fn search_recording(
|
||||
&self,
|
||||
title: &str,
|
||||
artist: &str,
|
||||
) -> IntegrationResult<RecordingSearchResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -285,7 +300,11 @@ impl MusicBrainzClient {
|
||||
Self::escape_query(title),
|
||||
Self::escape_query(artist)
|
||||
);
|
||||
let url = format!("{}/recording?query={}&fmt=json&limit=5", base, urlencoding::encode(&query));
|
||||
let url = format!(
|
||||
"{}/recording?query={}&fmt=json&limit=5",
|
||||
base,
|
||||
urlencoding::encode(&query)
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
self.check_response(&response).await;
|
||||
@@ -294,7 +313,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Get artist details with relationships and tags
|
||||
pub async fn get_artist_with_relations(&self, mbid: &str) -> IntegrationResult<ArtistFullResponse> {
|
||||
pub async fn get_artist_with_relations(
|
||||
&self,
|
||||
mbid: &str,
|
||||
) -> IntegrationResult<ArtistFullResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -330,11 +352,18 @@ impl MusicBrainzClient {
|
||||
.filter(|tag| tag.count.unwrap_or(0) > 0)
|
||||
.collect();
|
||||
tags.sort_by(|a, b| b.count.unwrap_or(0).cmp(&a.count.unwrap_or(0)));
|
||||
Ok(tags.into_iter().map(|tag| tag.name.to_lowercase()).collect())
|
||||
Ok(tags
|
||||
.into_iter()
|
||||
.map(|tag| tag.name.to_lowercase())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Search artists by tag (genre)
|
||||
pub async fn search_artists_by_tag(&self, tag: &str, limit: usize) -> IntegrationResult<ArtistSearchResponse> {
|
||||
pub async fn search_artists_by_tag(
|
||||
&self,
|
||||
tag: &str,
|
||||
limit: usize,
|
||||
) -> IntegrationResult<ArtistSearchResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -343,7 +372,9 @@ impl MusicBrainzClient {
|
||||
let query = format!("tag:\"{}\"", Self::escape_query(tag));
|
||||
let url = format!(
|
||||
"{}/artist?query={}&fmt=json&limit={}",
|
||||
base, urlencoding::encode(&query), limit
|
||||
base,
|
||||
urlencoding::encode(&query),
|
||||
limit
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
@@ -373,7 +404,10 @@ impl MusicBrainzClient {
|
||||
);
|
||||
let url = format!(
|
||||
"{}/artist?query={}&fmt=json&limit={}&offset={}",
|
||||
base, urlencoding::encode(&query), limit, offset
|
||||
base,
|
||||
urlencoding::encode(&query),
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
@@ -382,12 +416,18 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Search releases by barcode (UPC/EAN)
|
||||
pub async fn search_release_by_barcode(&self, barcode: &str) -> IntegrationResult<ReleaseSearchResponse> {
|
||||
pub async fn search_release_by_barcode(
|
||||
&self,
|
||||
barcode: &str,
|
||||
) -> IntegrationResult<ReleaseSearchResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
let base = self.base_url().await;
|
||||
let url = format!("{}/release?query=barcode:{}&fmt=json&limit=5", base, barcode);
|
||||
let url = format!(
|
||||
"{}/release?query=barcode:{}&fmt=json&limit=5",
|
||||
base, barcode
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
let response = self.handle_response_status(response).await?;
|
||||
@@ -395,7 +435,11 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Search releases by title and artist
|
||||
pub async fn search_release(&self, title: &str, artist: &str) -> IntegrationResult<ReleaseSearchResponse> {
|
||||
pub async fn search_release(
|
||||
&self,
|
||||
title: &str,
|
||||
artist: &str,
|
||||
) -> IntegrationResult<ReleaseSearchResponse> {
|
||||
self.search_releases_extended(title, artist, None, 5).await
|
||||
}
|
||||
|
||||
@@ -428,7 +472,9 @@ impl MusicBrainzClient {
|
||||
let limit = limit.min(25).max(1);
|
||||
let url = format!(
|
||||
"{}/release?query={}&fmt=json&limit={}",
|
||||
base, urlencoding::encode(&query), limit
|
||||
base,
|
||||
urlencoding::encode(&query),
|
||||
limit
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
@@ -437,7 +483,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Get full release details including tracks
|
||||
pub async fn get_release_with_tracks(&self, release_id: &str) -> IntegrationResult<ReleaseFullResponse> {
|
||||
pub async fn get_release_with_tracks(
|
||||
&self,
|
||||
release_id: &str,
|
||||
) -> IntegrationResult<ReleaseFullResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -475,7 +524,11 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Search for an area by name
|
||||
pub async fn search_area(&self, name: &str, area_type: Option<&str>) -> IntegrationResult<AreaSearchResponse> {
|
||||
pub async fn search_area(
|
||||
&self,
|
||||
name: &str,
|
||||
area_type: Option<&str>,
|
||||
) -> IntegrationResult<AreaSearchResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -490,7 +543,11 @@ impl MusicBrainzClient {
|
||||
format!("area:\"{}\"", Self::escape_query(name))
|
||||
};
|
||||
|
||||
let url = format!("{}/area?query={}&fmt=json&limit=5", base, urlencoding::encode(&query));
|
||||
let url = format!(
|
||||
"{}/area?query={}&fmt=json&limit=5",
|
||||
base,
|
||||
urlencoding::encode(&query)
|
||||
);
|
||||
|
||||
let response = self.client.get(&url).send().await?;
|
||||
let response = self.handle_response_status(response).await?;
|
||||
@@ -498,7 +555,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Look up an area and its parent relationships
|
||||
pub async fn get_area_with_relations(&self, area_id: &str) -> IntegrationResult<AreaDetailResponse> {
|
||||
pub async fn get_area_with_relations(
|
||||
&self,
|
||||
area_id: &str,
|
||||
) -> IntegrationResult<AreaDetailResponse> {
|
||||
self.check_enabled().await?;
|
||||
self.rate_limiter.wait().await;
|
||||
|
||||
@@ -511,7 +571,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
/// Resolve a city area to its parent subdivision (state/region)
|
||||
pub async fn resolve_parent_subdivision(&self, area_id: &str) -> IntegrationResult<Option<(String, String)>> {
|
||||
pub async fn resolve_parent_subdivision(
|
||||
&self,
|
||||
area_id: &str,
|
||||
) -> IntegrationResult<Option<(String, String)>> {
|
||||
let mut current_id = area_id.to_string();
|
||||
let mut path: Vec<String> = Vec::new();
|
||||
let max_hops = 5;
|
||||
@@ -539,7 +602,10 @@ impl MusicBrainzClient {
|
||||
}
|
||||
|
||||
let has_country_parent = parents.iter().any(|p| {
|
||||
p.area_type.as_deref().map(|t| t.eq_ignore_ascii_case("country")).unwrap_or(false)
|
||||
p.area_type
|
||||
.as_deref()
|
||||
.map(|t| t.eq_ignore_ascii_case("country"))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
if has_country_parent {
|
||||
@@ -559,7 +625,10 @@ impl MusicBrainzClient {
|
||||
let next = parents
|
||||
.iter()
|
||||
.find(|p| {
|
||||
p.area_type.as_deref().map(|t| t.eq_ignore_ascii_case("subdivision")).unwrap_or(false)
|
||||
p.area_type
|
||||
.as_deref()
|
||||
.map(|t| t.eq_ignore_ascii_case("subdivision"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.or_else(|| {
|
||||
parents.iter().find(|p| {
|
||||
@@ -570,8 +639,12 @@ impl MusicBrainzClient {
|
||||
.or_else(|| parents.first());
|
||||
|
||||
match next {
|
||||
Some(parent) => { current_id = parent.id.clone(); }
|
||||
None => { return Ok(None); }
|
||||
Some(parent) => {
|
||||
current_id = parent.id.clone();
|
||||
}
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +667,10 @@ impl MusicBrainzClient {
|
||||
// Placeholder for response logging/metrics
|
||||
}
|
||||
|
||||
async fn handle_response_status(&self, response: reqwest::Response) -> IntegrationResult<reqwest::Response> {
|
||||
async fn handle_response_status(
|
||||
&self,
|
||||
response: reqwest::Response,
|
||||
) -> IntegrationResult<reqwest::Response> {
|
||||
if response.status().is_success() {
|
||||
return Ok(response);
|
||||
}
|
||||
@@ -603,7 +679,10 @@ impl MusicBrainzClient {
|
||||
return Err(IntegrationError::RateLimited(60));
|
||||
}
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
Err(IntegrationError::internal(format!("MusicBrainz API error {}: {}", status, text)))
|
||||
Err(IntegrationError::internal(format!(
|
||||
"MusicBrainz API error {}: {}",
|
||||
status, text
|
||||
)))
|
||||
}
|
||||
|
||||
/// Escape special characters in Lucene queries
|
||||
|
||||
Reference in new issue
Block a user