fix(local): the ephemeral pane names, illustrates and ORDERS its discs
Opening a box set through "Open ▾" — the quick path for something outside
the library — listed its 13 discs as Disc 1, 10, 11, 12, 13, 2, 3 …, each
with the same heading and the same cover. The Folders grid gets all three
right from the same files, which is what proved it was fixable.
THREE DEFECTS, one per symptom:
ORDER. Blocks are grouped in a BTreeMap keyed on the STRING
"{album}#disc{n}", which sorts lexicographically. The final
`sort_by(title)` was meant to impose real order, but every block of a
box carries the same title, so it tied on every pair and the string key
showed through.
TITLE. `album_group_title` is the group name with the disc suffix
STRIPPED — identical for all 13 blocks. That stripping is what makes a
box ONE album and must stay.
COVER. `artwork_path` is biased to the album ROOT by the scanner
(+5 bonus, strict >), so every block resolved to the same file.
The name and cover helpers now live in `local_rows` and are shared with
the album page's divider, which had grown its own copies yesterday.
AND THE ORDERING FIX HAD A SECOND ACT WORTH RECORDING: giving each block
its own title made `sort_by(title)` start to bite, and it sorted by the
DISC NAME — putting "Complete Song Collection 1" (disc 11) first.
Measured, not predicted. So the sort key is the BOX's title with the disc
NUMBER as the tiebreak: ordering discs by anything except their number is
the same mistake twice.
Verified against the owner's own box: blocks now run TV Series
Soundtrack #01, #02, Movie Saint Seiya OST, #03, Movie Kamigami…, #04 —
i.e. discs 1,2,3,4,5,6 in order, matching the Folders grid, each with its
own cover.
ALSO, the 15 s open: it is not painting, it is 247 sequential
`canonicalize` + tag reads over NFS. Only the READS are now parallel
(`std::thread::scope`, no new dependency, ≤8 workers); the CUE dedup, the
APE skip, id assignment, the artwork caches and the push order all stay
serial and in scan order, because ids are observable. Measured 14.8 s →
8.9 s on the owner's NAS. Better, NOT solved — something else holds the
remaining half and I have not identified it.
cc
This commit is contained in:
4 files changed
+180
-45
No files matched your search
@@ -265,13 +265,64 @@ impl EphemeralLibraryState {
|
||||
}
|
||||
}
|
||||
|
||||
for audio_file in &scan.audio_files {
|
||||
// ── THE FILE READS RUN IN PARALLEL; EVERYTHING ELSE STAYS SERIAL ──
|
||||
//
|
||||
// Opening a folder of 247 FLACs on a NAS took ~15 s between "Scanned"
|
||||
// and "ephemeral opened" (owner's log, 2026-08-22), on what is meant to
|
||||
// be the QUICK way to play something outside the library. The cost is
|
||||
// not computation: it is 247 sequential `canonicalize` + tag-read round
|
||||
// trips over the network, each a few tens of milliseconds, one after
|
||||
// another.
|
||||
//
|
||||
// So only the READS move. The bookkeeping below — the CUE-dedup check,
|
||||
// the APE skip, id assignment, the artwork caches, the push order — is
|
||||
// untouched and still runs in scan order, because ids and ordering are
|
||||
// observable and a parallel loop must not renumber anything.
|
||||
//
|
||||
// `std::thread::scope`, not a new dependency: this is I/O-bound, the
|
||||
// work items are independent, and borrowing `scan.audio_files` across
|
||||
// the scope needs no Arc.
|
||||
let probed: Vec<(std::path::PathBuf, Result<LocalTrack, LibraryError>)> = {
|
||||
let files: &[std::path::PathBuf] = &scan.audio_files;
|
||||
let workers = std::thread::available_parallelism()
|
||||
.map(|n| n.get().min(8))
|
||||
.unwrap_or(4)
|
||||
.max(1);
|
||||
let chunk = files.len().div_ceil(workers).max(1);
|
||||
let mut out: Vec<Vec<(std::path::PathBuf, Result<LocalTrack, LibraryError>)>> =
|
||||
Vec::new();
|
||||
std::thread::scope(|sc| {
|
||||
let handles: Vec<_> = files
|
||||
.chunks(chunk)
|
||||
.map(|part| {
|
||||
sc.spawn(move || {
|
||||
part.iter()
|
||||
.map(|f| {
|
||||
let canonical = std::fs::canonicalize(f)
|
||||
.unwrap_or_else(|_| f.clone());
|
||||
(canonical, MetadataExtractor::extract(f))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for h in handles {
|
||||
// A panicking worker must not take the whole open down: the
|
||||
// chunk is dropped and its files are simply absent, which
|
||||
// the counters below already report as skipped.
|
||||
out.push(h.join().unwrap_or_default());
|
||||
}
|
||||
});
|
||||
out.into_iter().flatten().collect()
|
||||
};
|
||||
|
||||
for (audio_file, (canonical_audio, extracted)) in
|
||||
scan.audio_files.iter().zip(probed.into_iter())
|
||||
{
|
||||
// Skip audio files that were already exploded into tracks via
|
||||
// a CUE sheet — listing them again as a single row would
|
||||
// duplicate the album and confuse playback (the CUE-derived
|
||||
// track ids are the canonical ones).
|
||||
let canonical_audio =
|
||||
std::fs::canonicalize(audio_file).unwrap_or_else(|_| audio_file.clone());
|
||||
if cue_referenced_audio.contains(&canonical_audio) {
|
||||
continue;
|
||||
}
|
||||
@@ -293,7 +344,7 @@ impl EphemeralLibraryState {
|
||||
skipped_files += 1;
|
||||
continue;
|
||||
}
|
||||
match MetadataExtractor::extract(audio_file) {
|
||||
match extracted {
|
||||
Ok(mut track) => {
|
||||
track.id = inner.next_id;
|
||||
inner.next_id += 1;
|
||||
|
||||
Reference in new issue
Block a user