use serde::Deserialize; const BASE_URL: &str = "https://ws.audioscrobbler.com/2.0/"; pub struct LastfmClient { api_key: String, } pub struct SimilarArtist { pub name: String, pub mbid: Option, pub match_score: f64, } // Last.fm returns {"error": N, "message": "..."} on failure #[derive(Deserialize)] struct ApiError { #[allow(dead_code)] error: u32, message: String, } // Deserialization structs for the Last.fm API responses #[derive(Deserialize)] struct SimilarArtistsResponse { similarartists: SimilarArtistsWrapper, } #[derive(Deserialize)] struct SimilarArtistsWrapper { artist: Vec, } #[derive(Deserialize)] struct ArtistEntry { name: String, mbid: Option, #[serde(rename = "match")] match_score: String, } impl LastfmClient { pub fn new(api_key: String) -> Self { Self { api_key } } pub fn get_similar_artists( &self, artist_mbid: &str, ) -> Result, Box> { let url = format!( "{}?method=artist.getSimilar&mbid={}&api_key={}&limit=500&format=json", BASE_URL, artist_mbid, self.api_key ); let body: String = ureq::get(&url).call()?.body_mut().read_to_string()?; if let Ok(err) = serde_json::from_str::(&body) { eprintln!(" Last.fm: {}", err.message); return Ok(Vec::new()); } let resp: SimilarArtistsResponse = serde_json::from_str(&body)?; Ok(resp .similarartists .artist .into_iter() .map(|a| { let mbid = a.mbid.filter(|s| !s.is_empty()); SimilarArtist { name: a.name, mbid, match_score: a.match_score.parse().unwrap_or(0.0), } }) .collect()) } }