Search for both mbid and name

This commit is contained in:
Connor Johnstone
2026-03-03 13:45:52 -05:00
parent 60a1d704dd
commit 51ededc612

View File

@@ -92,7 +92,9 @@ impl LastfmClient {
.replace('\u{2012}', "-") .replace('\u{2012}', "-")
.replace('\u{2013}', "-") .replace('\u{2013}', "-")
.replace('\u{2014}', "-") .replace('\u{2014}', "-")
.replace('\u{2015}', "-"); .replace('\u{2015}', "-")
.replace('\u{2018}', "'")
.replace('\u{2019}', "'");
let encoded = urlencoding::encode(&name); let encoded = urlencoding::encode(&name);
let url = format!( let url = format!(
"{}?method={}&artist={}&api_key={}{}&format=json", "{}?method={}&artist={}&api_key={}{}&format=json",
@@ -103,23 +105,21 @@ impl LastfmClient {
Ok(None) Ok(None)
} }
/// Try fetching by MBID first, fall back to artist name. fn parse_similar_artists(body: &str) -> Result<Vec<SimilarArtist>, Box<dyn std::error::Error>> {
fn fetch_with_fallback( let resp: SimilarArtistsResponse = serde_json::from_str(body)?;
&self, Ok(resp
method: &str, .similarartists
artist_mbid: &str, .artist
artist_name: Option<&str>, .into_iter()
extra_params: &str, .map(|a| {
) -> Result<Option<String>, Box<dyn std::error::Error>> { let mbid = a.mbid.filter(|s| !s.is_empty());
let url = format!( SimilarArtist {
"{}?method={}&mbid={}&api_key={}{}&format=json", name: a.name,
BASE_URL, method, artist_mbid, self.api_key, extra_params mbid,
); match_score: a.match_score.parse().unwrap_or(0.0),
if let Some(body) = self.fetch_or_none(&url)? {
return Ok(Some(body));
} }
})
self.fetch_by_name(method, artist_name, extra_params) .collect())
} }
pub fn get_similar_artists( pub fn get_similar_artists(
@@ -127,51 +127,45 @@ impl LastfmClient {
artist_mbid: &str, artist_mbid: &str,
artist_name: Option<&str>, artist_name: Option<&str>,
) -> Result<Vec<SimilarArtist>, Box<dyn std::error::Error>> { ) -> Result<Vec<SimilarArtist>, Box<dyn std::error::Error>> {
let Some(body) = self.fetch_with_fallback( let method = "artist.getSimilar";
"artist.getSimilar", let extra = "&limit=500";
artist_mbid,
artist_name, // Try MBID lookup
"&limit=500", let mbid_url = format!(
)? else { "{}?method={}&mbid={}&api_key={}{}&format=json",
return Ok(Vec::new()); BASE_URL, method, artist_mbid, self.api_key, extra
);
let mbid_results = match self.fetch_or_none(&mbid_url)? {
Some(body) => Self::parse_similar_artists(&body).unwrap_or_default(),
None => Vec::new(),
}; };
let resp: SimilarArtistsResponse = serde_json::from_str(&body)?; // Try name lookup and return whichever has more results
let results: Vec<SimilarArtist> = resp let name_results = match self.fetch_by_name(method, artist_name, extra)? {
.similarartists Some(body) => Self::parse_similar_artists(&body).unwrap_or_default(),
.artist None => Vec::new(),
.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();
// MBID lookup can return valid but empty results; retry with name if name_results.len() > mbid_results.len() {
if results.is_empty() { Ok(name_results)
if let Some(body) = self.fetch_by_name("artist.getSimilar", artist_name, "&limit=500")? { } else {
let resp: SimilarArtistsResponse = serde_json::from_str(&body)?; Ok(mbid_results)
return 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());
} }
} }
Ok(results) fn parse_top_tracks(body: &str) -> Result<Vec<TopTrack>, Box<dyn std::error::Error>> {
let resp: TopTracksResponse = serde_json::from_str(body)?;
Ok(resp
.toptracks
.track
.into_iter()
.map(|t| TopTrack {
name: t.name,
mbid: t.mbid.filter(|s| !s.is_empty()),
playcount: t.playcount.parse().unwrap_or(0),
listeners: t.listeners.parse().unwrap_or(0),
})
.collect())
} }
pub fn get_top_tracks( pub fn get_top_tracks(
@@ -179,45 +173,29 @@ impl LastfmClient {
artist_mbid: &str, artist_mbid: &str,
artist_name: Option<&str>, artist_name: Option<&str>,
) -> Result<Vec<TopTrack>, Box<dyn std::error::Error>> { ) -> Result<Vec<TopTrack>, Box<dyn std::error::Error>> {
let Some(body) = self.fetch_with_fallback( let method = "artist.getTopTracks";
"artist.getTopTracks", let extra = "&limit=1000";
artist_mbid,
artist_name, // Try MBID lookup
"&limit=1000", let mbid_url = format!(
)? else { "{}?method={}&mbid={}&api_key={}{}&format=json",
return Ok(Vec::new()); BASE_URL, method, artist_mbid, self.api_key, extra
);
let mbid_results = match self.fetch_or_none(&mbid_url)? {
Some(body) => Self::parse_top_tracks(&body).unwrap_or_default(),
None => Vec::new(),
}; };
let resp: TopTracksResponse = serde_json::from_str(&body)?; // Try name lookup and return whichever has more results
let results: Vec<TopTrack> = resp let name_results = match self.fetch_by_name(method, artist_name, extra)? {
.toptracks Some(body) => Self::parse_top_tracks(&body).unwrap_or_default(),
.track None => Vec::new(),
.into_iter() };
.map(|t| TopTrack {
name: t.name,
mbid: t.mbid.filter(|s| !s.is_empty()),
playcount: t.playcount.parse().unwrap_or(0),
listeners: t.listeners.parse().unwrap_or(0),
})
.collect();
if results.is_empty() { if name_results.len() > mbid_results.len() {
if let Some(body) = self.fetch_by_name("artist.getTopTracks", artist_name, "&limit=1000")? { Ok(name_results)
let resp: TopTracksResponse = serde_json::from_str(&body)?; } else {
return Ok(resp Ok(mbid_results)
.toptracks
.track
.into_iter()
.map(|t| TopTrack {
name: t.name,
mbid: t.mbid.filter(|s| !s.is_empty()),
playcount: t.playcount.parse().unwrap_or(0),
listeners: t.listeners.parse().unwrap_or(0),
})
.collect());
} }
} }
Ok(results)
}
} }