library: the page lit by its own records, and portraits that prove out

The expander reserves once on the page root instead of standing min-heights
on the grid; shelf heads carry artist portraits keyed by path; every record
tints its own card, expander and glow from its cover; the artwork opens the
shared viewer; the banner wash reads the playing record and actually paints
(isolation, the artist page's own fix); descriptions animate on both pages
via partAnim. Portrait resolution rejects placeholder rows and never stores
a silhouette (pick_artist_portrait, 3 tests); repair re-resolves the missing.
This commit is contained in:
KeelrLeek committed 2026-08-06 17:21:23 -05:00
1 parent 741c4ae6dc
commit 185c29fc33
5 files changed
+1068 -49

No files matched your search

+47
View File
@@ -280,9 +280,56 @@ async fn scan_downloads(app: &tauri::AppHandle) {
process_file(app, &path, None).await;
}
}
repair_artist_portraits(app).await;
let _ = app.emit("soulseek:library-changed", ());
}
/// Artist portraits are written ONCE, by `process_file` at import. This scan
/// skips every file it already knows, so a portrait that later goes missing
/// from disk — a cleared art directory, a half-copied profile — leaves the
/// database pointing at a file that will never come back, and the shelf head
/// falls back to its icon forever.
///
/// Repair exactly that: an artist whose stored path is NULL or no longer on
/// disk. Untouched portraits cost nothing, and an affected artist costs one
/// paced Deezer read (shared budget, same as every other read here).
///
/// Deliberately NOT a re-resolve of portraits that DO exist. That would repair
/// the other half of this problem — entries stored before `fetch_artist_art`
/// learned to rank candidates and reject Deezer's empty-md5 placeholder — but
/// it is a request per artist on every scan, and it would overwrite a portrait
/// the user is already looking at. Dropping the `needs` condition below is all
/// it would take, if that is ever wanted.
async fn repair_artist_portraits(app: &tauri::AppHandle) {
let state = app.state::<LibraryState>();
let stored = {
let lib = state.library.clone();
match tauri::async_runtime::spawn_blocking(move || lib.artist_portraits()).await {
Ok(Ok(rows)) => rows,
_ => return,
}
};
for (artist, path) in stored {
let needs = match path.as_deref() {
None => true,
Some(p) => !Path::new(p).exists(),
};
if !needs {
continue;
}
if let Some(fresh) = fetch_artist_art_cached(&state, &artist).await {
let lib = state.library.clone();
let name = artist.clone();
let updated =
tauri::async_runtime::spawn_blocking(move || lib.set_artist_art(&name, &fresh))
.await;
if let Ok(Ok(n)) = updated {
log::info!("artist portrait repaired for {artist} ({n} albums)");
}
}
}
}
fn collect_audio_files(dir: &Path, out: &mut Vec<PathBuf>, depth: u8) {
if depth > 6 {
return;