use super::*;
use crate::*;
pub(crate) fn spawn_library_fetch(
webapi: Arc<Mutex<WebApi>>,
tx: flume::Sender<(Section, Vec<LibItem>)>,
done_tx: flume::Sender<bool>,
) {
let token_opt = token_of(&webapi);
liblog(format!(
"spawn_library_fetch: token={}",
token_opt.as_ref().map_or("missing", |_| "ok")
));
std::thread::Builder::new()
.name("myx-library".to_string())
.spawn(move || {
liblog("worker: entered");
let Some(token) = token_opt else {
liblog("worker: no token; aborting");
let _ = done_tx.send(false);
return;
};
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(12))
.build()
.expect("build library HTTP client");
let mut got_any = false;
let track_from = |t: &serde_json::Value| -> Option<LibItem> {
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(),
))
};
let artist_from = |a: &serde_json::Value| -> Option<LibItem> {
Some(LibItem::ctx(
a["name"].as_str()?.to_string(),
String::new(),
a["uri"].as_str()?.to_string(),
))
};
let album_from = |a: &serde_json::Value| -> Option<LibItem> {
Some(LibItem::ctx(
a["name"].as_str()?.to_string(),
format!("album · {}", a["artists"][0]["name"].as_str().unwrap_or("")),
a["uri"].as_str()?.to_string(),
))
};
liblog("worker: fetching home/recent");
let mut home: Vec<LibItem> = Vec::new();
let recent5 = fetch_all_pages(
&client,
&format!("{API}/me/player/recently-played?limit=10"),
&token,
None,
1,
|it| track_from(&it["track"]),
);
if !recent5.is_empty() {
home.push(LibItem::header("Recently Played"));
home.extend(recent5.into_iter().take(6));
}
let top_tracks = fetch_all_pages(
&client,
&format!("{API}/me/top/tracks?limit=10"),
&token,
None,
1,
|t| track_from(t),
);
if !top_tracks.is_empty() {
home.push(LibItem::header("Your Top Tracks"));
home.extend(top_tracks.into_iter().take(8));
}
let top_artists = fetch_all_pages(
&client,
&format!("{API}/me/top/artists?limit=10"),
&token,
None,
1,
|a| artist_from(a),
);
if !top_artists.is_empty() {
home.push(LibItem::header("Your Top Artists"));
home.extend(top_artists.into_iter().take(6));
}
got_any |= !home.is_empty();
liblog(format!("worker: home done, {} rows", home.len()));
let _ = tx.send((Section::Home, home));
let recent = fetch_all_pages(
&client,
&format!("{API}/me/player/recently-played?limit=50"),
&token,
None,
1,
|it| track_from(&it["track"]),
);
got_any |= !recent.is_empty();
let _ = tx.send((Section::Recent, recent));
let playlists = fetch_all_pages(
&client,
&format!("{API}/me/playlists?limit=50"),
&token,
None,
10,
|it| {
Some(LibItem::ctx(
it["name"].as_str()?.to_string(),
playlist_subtitle(
it["owner"]["display_name"].as_str().unwrap_or(""),
playlist_total(it),
),
it["uri"].as_str()?.to_string(),
))
},
);
got_any |= !playlists.is_empty();
let _ = tx.send((Section::Playlists, playlists));
let albums = fetch_all_pages(
&client,
&format!("{API}/me/albums?limit=50"),
&token,
None,
10,
|it| album_from(&it["album"]),
);
got_any |= !albums.is_empty();
let _ = tx.send((Section::Albums, albums));
let artists = fetch_all_pages(
&client,
&format!("{API}/me/following?type=artist&limit=50"),
&token,
Some("artists"),
5,
|it| artist_from(it),
);
got_any |= !artists.is_empty();
let _ = tx.send((Section::Artists, artists));
let mut liked: Vec<LibItem> = vec![
LibItem::play("▶︎ Play Liked Songs".into(), "myx:action:liked-play".into()),
LibItem::header("Songs"),
];
let mut url = Some(format!("{API}/me/tracks?limit=50"));
let mut pages = 0;
while let Some(u) = url.take() {
if pages >= 100 {
break;
}
let Some(v) = get_json(&client, &u, &token) else {
break;
};
for it in v["items"].as_array().into_iter().flatten() {
if let Some(li) = track_from(&it["track"]) {
liked.push(li);
}
}
url = v["next"].as_str().map(String::from);
pages += 1;
if pages % 3 == 0 {
let _ = tx.send((Section::Liked, liked.clone()));
}
}
got_any |= liked.len() > 2; let _ = tx.send((Section::Liked, liked));
liblog(format!("worker: all done got_any={got_any}"));
let _ = done_tx.send(got_any);
})
.expect("spawn library worker");
}
pub(crate) fn parse_playlist_track(it: &serde_json::Value) -> Option<LibItem> {
let t = if it["item"].is_object() {
&it["item"]
} else {
&it["track"]
};
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(),
))
}
pub(crate) fn playlist_total(p: &serde_json::Value) -> Option<u64> {
p["items"]["total"]
.as_u64()
.or_else(|| p["tracks"]["total"].as_u64())
}
pub(crate) fn playlist_subtitle(owner: &str, total: Option<u64>) -> String {
match total {
Some(n) if owner.is_empty() => n.to_string(),
Some(n) => format!("{n} · {owner}"),
None => owner.to_string(),
}
}
pub(crate) fn fetch_all_pages(
client: &reqwest::blocking::Client,
first_url: &str,
token: &str,
nested: Option<&str>,
max_pages: usize,
parse: impl Fn(&serde_json::Value) -> Option<LibItem>,
) -> Vec<LibItem> {
let mut out = Vec::new();
let mut url = Some(first_url.to_string());
let mut pages = 0;
while let Some(u) = url.take() {
if pages >= max_pages {
break;
}
let Some(v) = get_json(client, &u, token) else {
break;
};
let node = match nested {
Some(k) => &v[k],
None => &v,
};
for it in node["items"].as_array().into_iter().flatten() {
if let Some(li) = parse(it) {
out.push(li);
}
}
url = node["next"].as_str().map(String::from);
pages += 1;
}
out
}