overlay + library: the preloader working on glass, album names that land on the right self-titled record, and covers warmed before you scroll to them

This commit is contained in:
KeelrLeek committed 2026-08-25 08:05:07 -05:00
1 parent ad2c912b71
commit 6a7a5c49c0
7 files changed
+534 -39

No files matched your search

+161 -20
View File
@@ -563,6 +563,36 @@ pub async fn connect_and_login(
&update.state,
DS::Complete { .. } | DS::Failed { .. } | DS::Cancelled
);
// Mirror every update for the debug overlay's
// QUEUE section — these transfers are otherwise
// invisible by design.
let state_label = match &update.state {
DS::Connecting => "connecting".to_string(),
DS::Requested => "requested".to_string(),
DS::Queued { .. } => "queued".to_string(),
DS::Transferring => "transferring".to_string(),
DS::Complete { .. } => "done".to_string(),
DS::Failed { reason } => format!("failed: {reason}"),
DS::Cancelled => "cancelled".to_string(),
};
let place = match &update.state {
DS::Queued { place } => *place,
_ => None,
};
crate::diagnostics::note_preload(
&event_app,
update.id,
crate::diagnostics::PreloadDl {
username: update.username.clone(),
path: update.filename.clone(),
state: state_label,
place,
received: update.received,
total: update.total,
speed_bps: update.speed_bps,
},
terminal,
);
if terminal {
let cache = ps.preloads.lock().unwrap().remove(&update.id);
let save_cache =
@@ -3829,6 +3859,9 @@ pub async fn play_stream(
.map(|(id, _)| *id);
if let Some(id) = preload_id {
players.preloads.lock().unwrap().remove(&id);
// From here the transfer is the CURRENT track's stream; the overlay's
// DL line (note_stream below) reports it, not the on-deck list.
crate::diagnostics::retire_preload(&app, id);
let progress = player::StreamProgress::new();
if let Ok(md) = std::fs::metadata(&partial) {
progress.update(md.len(), false, None);
@@ -4252,26 +4285,7 @@ pub async fn album_art(
let Some(edge) = max_edge else {
return Ok((std::fs::read(&probe_path).map_err(|e| e.to_string())?, None));
};
let target = probe_dir.and_then(|dir| {
let meta = std::fs::metadata(&probe_path).ok()?;
let mtime = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
let mut hash: u64 = 0xcbf29ce484222325;
for b in probe_path
.as_bytes()
.iter()
.chain(mtime.to_le_bytes().iter())
.chain(meta.len().to_le_bytes().iter())
{
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x100000001b3);
}
Some(dir.join(format!("{hash:016x}-{edge}.jpg")))
});
let target = probe_dir.and_then(|dir| art_thumb_target(&dir, &probe_path, edge));
if let Some(t) = &target {
if let Ok(cached) = std::fs::read(t) {
return Ok((cached, None));
@@ -4324,6 +4338,133 @@ pub async fn album_art(
/// requests from ever owning more than two cores.
static ART_DECODE_SLOTS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(2);
/// The disk-cache file for one (source, edge) thumbnail, keyed on
/// (path, mtime, len, edge) so a replaced cover self-invalidates. ONE
/// implementation on purpose: `album_art` reads through this key and
/// `library_art_prewarm` writes through it — if they computed it separately
/// and drifted, the warm work would be invisible to the tiles it was done
/// for. None when the source file cannot be statted.
fn art_thumb_target(dir: &std::path::Path, src: &str, edge: u32) -> Option<std::path::PathBuf> {
let meta = std::fs::metadata(src).ok()?;
let mtime = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
let mut hash: u64 = 0xcbf29ce484222325;
for b in src
.as_bytes()
.iter()
.chain(mtime.to_le_bytes().iter())
.chain(meta.len().to_le_bytes().iter())
{
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x100000001b3);
}
Some(dir.join(format!("{hash:016x}-{edge}.jpg")))
}
/// Make the thumbnail cache hold every library cover (and artist portrait) at
/// each requested edge, so the Library's first-ever scroll is a disk read
/// instead of a master decode — [{warm, decoded}] per edge, in the order
/// given, so the caller puts the size it paints first.
///
/// Already-warm entries cost one stat each. Cold ones decode ONE at a time,
/// through the same 2-permit gate the tiles use — a background warm can never
/// own more than one core, and an on-screen tile waits behind at most one
/// decode. Edges walk outermost so the first edge finishes across the whole
/// library before the next begins.
#[tauri::command]
pub async fn library_art_prewarm(
app: tauri::AppHandle,
lib: State<'_, LibraryState>,
edges: Vec<u32>,
) -> Result<Vec<(usize, usize)>, String> {
let Some(thumb_dir) = app
.path()
.app_cache_dir()
.ok()
.map(|d| d.join("art-thumbs-v1"))
else {
return Ok(vec![]);
};
let l = lib.library.clone();
let albums =
tauri::async_runtime::spawn_blocking(move || l.list_albums().map_err(|e| e.to_string()))
.await
.map_err(|e| e.to_string())??;
let mut paths: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
for a in &albums {
// Portraits too: the shelf heads load through the same tile path.
for p in [a.art_path.as_ref(), a.artist_art_path.as_ref()]
.into_iter()
.flatten()
{
if seen.insert(p.clone()) {
paths.push(p.clone());
}
}
}
let mut out = Vec::new();
for edge in edges {
if edge == 0 {
out.push((0, 0));
continue;
}
// One pass of cheap stats finds the cold set; the warm majority never
// touches a permit (the same shape as album_art's phase 1).
let dir = thumb_dir.clone();
let batch = paths.clone();
let (warm, cold) = tauri::async_runtime::spawn_blocking(move || {
let mut warm = 0usize;
let mut cold: Vec<(String, std::path::PathBuf)> = Vec::new();
for path in &batch {
match art_thumb_target(&dir, path, edge) {
Some(t) if t.exists() => warm += 1,
Some(t) => cold.push((path.clone(), t)),
None => {}
}
}
(warm, cold)
})
.await
.map_err(|e| e.to_string())?;
let mut decoded = 0usize;
for (path, target) in cold {
let permit = ART_DECODE_SLOTS
.acquire()
.await
.map_err(|e| e.to_string())?;
let ok = tauri::async_runtime::spawn_blocking(move || {
let Ok(bytes) = std::fs::read(&path) else {
return false;
};
// Same fallback as album_art: an undecodable or already-small
// source caches its original bytes, so the discovery is paid
// once there too.
let out = match pipeline::thumbnail_jpeg(&bytes, edge) {
Ok(Some(thumb)) => thumb,
_ => bytes,
};
if let Some(dir) = target.parent() {
let _ = std::fs::create_dir_all(dir);
}
std::fs::write(&target, &out).is_ok()
})
.await
.map_err(|e| e.to_string())?;
drop(permit);
if ok {
decoded += 1;
}
}
out.push((warm, decoded));
}
Ok(out)
}
/// The cover a streamed track carries INSIDE its own bytes, extracted to a
/// file the ordinary art path can read.
///