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.
///
+60
View File
@@ -6,6 +6,7 @@
//! Process figures aggregate the WHOLE process tree: the WebView2 children
//! do the actual rendering, so app.exe alone would tell a comforting lie.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;
@@ -27,6 +28,48 @@ struct Inner {
/// StreamProgress — the overlay must read download truth at the source,
/// not the UI store's window-activity-gated mirror.
current_stream: Option<(u64, Arc<StreamProgress>)>,
/// On-deck pre-buffer transfers by transfer id, mirrored from the
/// download event stream (those updates are otherwise swallowed — no
/// player, no Downloads view). The Instant is set when the transfer
/// reaches a terminal state, so finished rows linger long enough to be
/// seen and then fall off.
preloads: HashMap<u64, (PreloadDl, Option<Instant>)>,
}
/// One on-deck transfer as the overlay sees it.
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PreloadDl {
pub username: String,
/// Full remote path — the frontend matches queue rows by this.
pub path: String,
pub state: String,
/// Remote queue position, when the peer reported one.
pub place: Option<u32>,
pub received: u64,
pub total: u64,
pub speed_bps: u64,
}
/// Called from the download event stream for every preload update.
pub fn note_preload(app: &tauri::AppHandle, id: u64, dl: PreloadDl, terminal: bool) {
if let Some(state) = app.try_state::<DiagState>() {
let finished = if terminal { Some(Instant::now()) } else { None };
state
.0
.lock()
.expect("diag lock")
.preloads
.insert(id, (dl, finished));
}
}
/// Called when a play path takes the transfer over: it is the CURRENT
/// track's stream from here on (note_stream reports it), not an on-deck row.
pub fn retire_preload(app: &tauri::AppHandle, id: u64) {
if let Some(state) = app.try_state::<DiagState>() {
state.0.lock().expect("diag lock").preloads.remove(&id);
}
}
/// Called by the play paths: this track streams from a peer.
@@ -51,6 +94,7 @@ pub fn init(app: &tauri::AppHandle) {
gpu: GpuCounter::open(),
started: Instant::now(),
current_stream: None,
preloads: HashMap::new(),
})));
}
@@ -98,6 +142,9 @@ pub struct DebugStats {
pub preview_volume: Option<f32>,
/// The active preview clip's loudness-normalization gain in dB.
pub preview_norm_db: Option<f32>,
/// On-deck pre-buffer transfers, in arrival order (finished rows linger
/// ~15s so a completion is visible before it drops off).
pub preload_dl: Vec<PreloadDl>,
}
#[tauri::command]
@@ -167,6 +214,18 @@ pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugS
let tree_u32: Vec<u32> = tree.iter().map(|p| p.as_u32()).collect();
let gpu_sample = inner.gpu.as_mut().and_then(|g| g.sample(&tree_u32));
// On-deck transfers: drop terminal rows after they've been visible a
// while, then report the rest in arrival (transfer-id) order.
inner
.preloads
.retain(|_, v| v.1.is_none_or(|t| t.elapsed().as_secs() < 15));
let mut preload_ids: Vec<u64> = inner.preloads.keys().copied().collect();
preload_ids.sort_unstable();
let preload_dl: Vec<PreloadDl> = preload_ids
.iter()
.filter_map(|id| inner.preloads.get(id).map(|(dl, _)| dl.clone()))
.collect();
DebugStats {
cpu_process,
cpu_system: inner.sys.global_cpu_usage(),
@@ -201,6 +260,7 @@ pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugS
main_fade_gain: app
.try_state::<crate::playback::PlayerState>()
.map(|p| p.player.fade_gain()),
preload_dl,
}
}
+1
View File
@@ -88,6 +88,7 @@ pub fn run() {
commands::library_album_stats,
commands::album_tracks,
commands::album_art,
commands::library_art_prewarm,
commands::stream_embedded_art,
commands::refresh_album_art,
commands::refresh_all_album_art,