use super::*;
use crate::*;
pub(crate) fn spawn_detail_fetch(
webapi: Arc<Mutex<WebApi>>,
uri: String,
name: String,
tx: flume::Sender<(String, String, Vec<LibItem>)>,
) {
tokio::task::spawn_blocking(move || {
if let Some(token) = token_of(&webapi) {
let (title, items) = fetch_detail_blocking(&token, &uri, &name);
let _ = tx.send((uri, title, items));
}
});
}
pub(crate) fn artist_top_tracks(
client: &reqwest::blocking::Client,
token: &str,
id: &str,
name: &str,
) -> Vec<LibItem> {
let url = format!(
"{API}/search?q={}&type=track&limit=10",
urlencode(&format!("artist:{name}"))
);
let Some(v) = get_json_cached(client, &url, token) else {
return Vec::new();
};
v["tracks"]["items"]
.as_array()
.into_iter()
.flatten()
.filter(|t| {
t["artists"]
.as_array()
.is_some_and(|a| a.iter().any(|x| x["id"].as_str() == Some(id)))
})
.filter_map(|t| {
Some(LibItem::track(
t["name"].as_str()?.to_string(),
t["artists"][0]["name"].as_str().unwrap_or("").to_string(),
t["uri"].as_str()?.to_string(),
))
})
.collect()
}
pub(crate) fn artist_albums(
client: &reqwest::blocking::Client,
token: &str,
id: &str,
) -> Vec<LibItem> {
let mut seen = std::collections::HashSet::new();
let mut albums: Vec<(String, LibItem)> = Vec::new();
let mut url = Some(format!(
"{API}/artists/{id}/albums?include_groups=album,single&limit=10"
));
let mut pages = 0;
while let Some(u) = url.take() {
if pages >= 3 {
break; }
let Some(v) = get_json_cached(client, &u, token) else {
break;
};
for a in v["items"].as_array().into_iter().flatten() {
let (Some(aname), Some(auri)) = (a["name"].as_str(), a["uri"].as_str()) else {
continue;
};
if !seen.insert(aname.to_lowercase()) {
continue;
}
let date = a["release_date"].as_str().unwrap_or("").to_string();
let year = date.split('-').next().unwrap_or("").to_string();
albums.push((
date,
LibItem::ctx(aname.to_string(), year, auri.to_string()),
));
}
url = v["next"].as_str().map(String::from);
pages += 1;
}
albums.sort_by(|x, y| y.0.cmp(&x.0)); albums.into_iter().map(|(_, it)| it).collect()
}
pub(crate) fn fetch_detail_blocking(token: &str, uri: &str, name: &str) -> (String, Vec<LibItem>) {
let client = http_client();
let mut parts = uri.split(':');
parts.next(); let kind = parts.next().unwrap_or("");
let id = parts.next().unwrap_or("");
let mut items = vec![LibItem::play(format!("▶︎ Play {name}"), uri.to_string())];
liblog(format!("detail: kind={kind} id={id} uri={uri}"));
match kind {
"artist" => {
let tracks = artist_top_tracks(&client, token, id, name);
if !tracks.is_empty() {
items.push(LibItem::header("Popular"));
items.extend(tracks);
}
let albums = artist_albums(&client, token, id);
if !albums.is_empty() {
items.push(LibItem::header("Albums"));
items.extend(albums);
}
if items.len() == 1 {
items.push(LibItem::header("nothing loaded — API error or quota"));
}
}
"album" => {
if let Some(v) = get_json_cached(
&client,
&format!("{API}/albums/{id}/tracks?limit=50"),
token,
) {
for t in v["items"].as_array().into_iter().flatten() {
if let (Some(n), Some(u)) = (t["name"].as_str(), t["uri"].as_str()) {
items.push(LibItem::track(
n.to_string(),
t["artists"][0]["name"].as_str().unwrap_or("").to_string(),
u.to_string(),
));
}
}
}
}
"playlist" => {
let before = items.len();
items.extend(fetch_all_pages(
&client,
&format!("{API}/playlists/{id}/items?limit=100"),
token,
None, 10, parse_playlist_track,
));
if items.len() == before {
items.push(LibItem::header("no tracks — empty or restricted"));
}
}
_ => {}
}
liblog(format!("detail: {} rows for {uri}", items.len()));
(name.to_string(), items)
}