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:
1 parent
ad2c912b71
commit
6a7a5c49c0
7 files changed
+534
-39
No files matched your search
+161
-20
@@ -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.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user