Skip to content

Commit c494403

Browse files
authored
fix(cli): repaint the progress line through stalled reads (#366)
* fix(cli): repaint the progress line through stalled reads The styled progress line was driven only by scan events, so a read that did not return froze it: a 40 s stall showed the pre-scan text and the first redraw after it already read `Elapsed: 00:00:40`. A ticker thread now repaints the retained event about once a second while the scan thread is blocked, and five seconds of silence swaps the lead-in to `Still reading`, the wording the desktop shell uses for the same state. The plain (piped stderr) path is unchanged. The short-stream notice sentence moves to `ShortStreamFile::notice`, the one home the CLI, the desktop shell and the browser mirror now delegate to; the wording is pinned there, and each surface asserts only that it delegates. * test(cli): keep the notice out of the assert message * perf(cli): end the stall ticker on demand and skip it on a plain line * refactor(cli): make the stall ticker a type instead of a generic wrapper * test(cli): pin the tick interval, the ticker's stop, and drop a redundant guard
1 parent 7d967e8 commit c494403

4 files changed

Lines changed: 515 additions & 128 deletions

File tree

‎crates/bdinfo-rs-core/src/bdrom/shortfall.rs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,26 @@ impl ShortStreamFile {
8484
pub const fn missing_seconds(&self) -> f64 {
8585
self.declared_seconds - self.measured_seconds
8686
}
87+
88+
/// The one-line notice a surface raises for this file, beside the report:
89+
/// `00011.M2TS is shorter than declared: measured 500.0 s of 1640.0 s
90+
/// (1140.0 s missing)`.
91+
///
92+
/// The single home of that sentence — the CLI prints it on stderr, the
93+
/// desktop shell banners it and the browser mirror crosses it as a wire
94+
/// string, all through this method, so the three surfaces cannot word it
95+
/// differently. Seconds to one decimal, like every other span the surfaces
96+
/// show a human.
97+
#[must_use]
98+
pub fn notice(&self) -> String {
99+
format!(
100+
"{} is shorter than declared: measured {:.1} s of {:.1} s ({:.1} s missing)",
101+
self.file(),
102+
self.measured_seconds(),
103+
self.declared_seconds(),
104+
self.missing_seconds()
105+
)
106+
}
87107
}
88108

89109
/// Every stream file in `playlists` whose demuxed span fell materially short of
@@ -277,6 +297,21 @@ mod tests {
277297
assert_eq!(names, vec!["00011.M2TS".to_owned(), "00033.M2TS".to_owned()]);
278298
}
279299

300+
#[test]
301+
fn the_notice_spells_the_sentence_every_surface_raises() {
302+
// The authoritative byte pin of the shared wording: the CLI, the
303+
// desktop shell and the browser mirror all render this string through
304+
// `ShortStreamFile::notice`, and each asserts only that it delegates
305+
// here — this test is where the bytes are decided.
306+
let short: Vec<ShortStreamFile> =
307+
short_stream_files(&disc_of(vec![measured_clip("00011.M2TS", 1640.0, 500.0)]));
308+
let first = short.first().expect("the short file is reported");
309+
assert_eq!(
310+
first.notice(),
311+
"00011.M2TS is shorter than declared: measured 500.0 s of 1640.0 s (1140.0 s missing)"
312+
);
313+
}
314+
280315
#[test]
281316
fn the_missing_span_is_the_difference_of_the_two() {
282317
let short: Vec<ShortStreamFile> =

‎crates/bdinfo-rs-gui/src/flow.rs‎

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,6 @@ pub fn scan_notice(errors: usize) -> String {
348348
}
349349
}
350350

351-
/// The notice for one stream file the demux measured shorter than the disc
352-
/// declares — raised beside the report, because the locked report format has no
353-
/// line for it ([`bdinfo_rs_core::bdrom::shortfall`]).
354-
///
355-
/// The wording is shared with the other surfaces' copies of this format
356-
/// (the `bdinfo-rs` CLI's `short_stream_notice`, `bdinfo-rs-wasm`'s
357-
/// `mirror::short_stream_notice`); each copy is pinned by an identical test, so
358-
/// a change here reworks all three together or fails a sibling gate.
359-
fn short_stream_notice(short: &ShortStreamFile) -> String {
360-
format!(
361-
"{} is shorter than declared: measured {:.1} s of {:.1} s ({:.1} s missing)",
362-
short.file(),
363-
short.measured_seconds(),
364-
short.declared_seconds(),
365-
short.missing_seconds()
366-
)
367-
}
368-
369351
/// What a measured scan needs, read from the [`Flow`] before spawning the worker.
370352
///
371353
/// The input to re-open, the selected playlist names (the report order), and the
@@ -929,11 +911,12 @@ impl Flow {
929911
///
930912
/// Derived from the retained disc on every call rather than stored, so a
931913
/// later scan that replaces the measured playlists can never leave a stale
932-
/// notice behind.
914+
/// notice behind. The sentences come from [`ShortStreamFile::notice`], the
915+
/// one home of that wording.
933916
#[must_use]
934917
pub fn short_stream_notices(&self) -> Vec<String> {
935918
self.any_listing().map_or_else(Vec::new, |listing| {
936-
listing.bdrom.short_stream_files().iter().map(short_stream_notice).collect()
919+
listing.bdrom.short_stream_files().iter().map(ShortStreamFile::notice).collect()
937920
})
938921
}
939922

@@ -1198,7 +1181,7 @@ mod tests {
11981181
use bdinfo_rs_core::primitives::Pid;
11991182
use bdinfo_rs_core::stream::TsStreamType;
12001183

1201-
use super::{Flow, Stage};
1184+
use super::{Flow, ShortStreamFile, Stage};
12021185
use crate::live::{LivePlaylist, LiveStream};
12031186
use crate::model::{Sort, SortColumn, ViewSettings};
12041187
use crate::scan::{Input, Structural};
@@ -2278,7 +2261,7 @@ mod tests {
22782261
}
22792262

22802263
#[test]
2281-
fn a_measured_short_stream_file_surfaces_the_notice_with_the_shared_wording() {
2264+
fn a_measured_short_stream_file_surfaces_cores_notice_sentence() {
22822265
use bdinfo_rs_core::bdrom::disc::ClipStreamTally;
22832266

22842267
// No disc, and an unmeasured (structural) disc: silent.
@@ -2301,12 +2284,22 @@ mod tests {
23012284
packet_count: 8,
23022285
}];
23032286
let flow = flow.finished(1, "R".to_owned(), Arc::new(Vec::new()), measured);
2304-
// The exact sentence, pinned: the CLI and wasm crates carry the same
2305-
// format and pin the same bytes, which is what keeps the three
2306-
// surfaces' wording identical.
2307-
assert_eq!(
2308-
flow.short_stream_notices(),
2309-
["A.M2TS is shorter than declared: measured 500.0 s of 1640.0 s (1140.0 s missing)"]
2287+
// Delegation, not a second pin: the shell shows exactly what
2288+
// `ShortStreamFile::notice` says — the bytes themselves are pinned in
2289+
// core, once, so this sentence cannot fork per surface.
2290+
let notices = flow.short_stream_notices();
2291+
let from_core: Vec<String> = flow
2292+
.any_listing()
2293+
.expect("the measured listing")
2294+
.bdrom
2295+
.short_stream_files()
2296+
.iter()
2297+
.map(ShortStreamFile::notice)
2298+
.collect();
2299+
assert_eq!(notices, from_core);
2300+
assert!(
2301+
notices.first().is_some_and(|notice| notice.starts_with("A.M2TS is shorter")),
2302+
"the notice names the truncated file: {notices:?}"
23102303
);
23112304
}
23122305

‎crates/bdinfo-rs-wasm/src/mirror.rs‎

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -775,29 +775,15 @@ impl Disc {
775775
}
776776
}
777777

778-
/// The notice for one stream file the demux measured shorter than the disc
779-
/// declares — raised beside the report, because the locked report format has no
780-
/// line for it ([`bdinfo_rs_core::bdrom::shortfall`]).
781-
///
782-
/// The wording is shared with the other surfaces' copies of this format
783-
/// (the `bdinfo-rs` CLI's `short_stream_notice`, `bdinfo-rs-gui`'s
784-
/// `flow::short_stream_notice`); each copy is pinned by an identical test, so
785-
/// a change here reworks all three together or fails a sibling gate.
786-
fn short_stream_notice(short: &ShortStreamFile) -> String {
787-
format!(
788-
"{} is shorter than declared: measured {:.1} s of {:.1} s ({:.1} s missing)",
789-
short.file(),
790-
short.measured_seconds(),
791-
short.declared_seconds(),
792-
short.missing_seconds()
793-
)
794-
}
795-
796778
/// [`Disc::short_stream_notices`] for a scanned disc: the notice sentences, or
797779
/// `None` when no stream file is short — the field's "nothing to say" spelling,
798780
/// so a healthy disc's wire form carries no empty list.
781+
///
782+
/// The sentences come from [`ShortStreamFile::notice`], the one home of that
783+
/// wording; the mirror only decides whether there are any.
799784
fn short_stream_notices(bdrom: &BdRom) -> Option<Vec<String>> {
800-
let notices: Vec<String> = bdrom.short_stream_files().iter().map(short_stream_notice).collect();
785+
let notices: Vec<String> =
786+
bdrom.short_stream_files().iter().map(ShortStreamFile::notice).collect();
801787
(!notices.is_empty()).then_some(notices)
802788
}
803789

@@ -1264,8 +1250,8 @@ mod tests {
12641250

12651251
use super::{
12661252
Chapter, Clip, ClipStream, Disc, HiddenRule, MeasuredSnapshot, Playlist, ScanError,
1267-
ScanErrorReason, ScanOptions, ScanStage, Stream, borrowed_codec_alt_name, core_measured,
1268-
order,
1253+
ScanErrorReason, ScanOptions, ScanStage, ShortStreamFile, Stream, borrowed_codec_alt_name,
1254+
core_measured, order,
12691255
};
12701256

12711257
// One fixture value and one hand-written wire form per mirror type, composed
@@ -1619,7 +1605,7 @@ mod tests {
16191605
}
16201606

16211607
#[test]
1622-
fn a_short_stream_file_crosses_as_the_shared_notice_wording() {
1608+
fn a_short_stream_file_crosses_as_cores_notice_sentence() {
16231609
// A clip demuxed to 500 of its declared 1640 seconds, carrying the
16241610
// per-stream tally that marks its file as measured.
16251611
let clip = ClipSummary {
@@ -1632,28 +1618,21 @@ mod tests {
16321618
..a_core_bdrom()
16331619
};
16341620
let disc = Disc::from_scan(&bdrom, &[], true, &PlaylistFilter::default(), None);
1635-
// The exact sentence, pinned: the CLI and GUI crates carry the same
1636-
// format and pin the same bytes, which is what keeps the three
1637-
// surfaces' wording identical.
1638-
assert_eq!(
1639-
disc.short_stream_notices.as_deref(),
1640-
Some(
1641-
&["00011.M2TS is shorter than declared: measured 500.0 s of 1640.0 s (1140.0 s \
1642-
missing)"
1643-
.to_owned()][..]
1644-
)
1621+
// Delegation, not a second pin: the mirror carries exactly what
1622+
// `ShortStreamFile::notice` says — the bytes themselves are pinned in
1623+
// core, once, so this sentence cannot fork per surface.
1624+
let from_core: Vec<String> =
1625+
bdrom.short_stream_files().iter().map(ShortStreamFile::notice).collect();
1626+
assert_eq!(disc.short_stream_notices.as_deref(), Some(&from_core[..]));
1627+
assert!(
1628+
from_core.first().is_some_and(|notice| notice.starts_with("00011.M2TS is shorter")),
1629+
"the notice names the truncated file: {from_core:?}"
16451630
);
16461631

16471632
// The wire carries the sentences as a plain array, and they survive
16481633
// the round trip.
16491634
let wire = serde_json::to_value(&disc).expect("serialize the mirror");
1650-
assert_eq!(
1651-
wire.get("shortStreamNotices"),
1652-
Some(&json!([
1653-
"00011.M2TS is shorter than declared: measured 500.0 s of 1640.0 s (1140.0 s \
1654-
missing)"
1655-
]))
1656-
);
1635+
assert_eq!(wire.get("shortStreamNotices"), Some(&json!(from_core)));
16571636
let back: Disc = serde_json::from_value(wire).expect("deserialize the mirror");
16581637
assert_eq!(back, disc);
16591638

0 commit comments

Comments
 (0)