//! Pipeline-progress reporting for the rip pipeline. //! //! Architecture rule: ONE progress signal type. Every long-running //! pipeline operation (`Disc::copy`, `Disc::patch`, `verify_title`) emits the //! same [`PassProgress`] shape via the [`Progress`] trait. Consumers (autorip) //! compute their own single derived view from these fields and never reach //! into per-pass internals. //! //! Why this matters: pre-0.13.16 the API leaked `pos`, `bytes_good`, //! `work_done`, `bytes_pending`, `Finished/NonTrimmed` mapfile semantics — //! and consumers reinvented the math each time they wanted a percentage. //! UIs ended up reading one source while server-side computed from another, //! producing wrong percentages without anyone noticing. /// Identifies which pipeline phase the progress event belongs to. /// /// Consumers can render a phase-specific label (e.g. "Sweep", "Trim /// (reverse)", "Scrape", "Mux") or just use a generic "Pass N" label. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PassKind { /// `Disc::copy` — initial sweep across the entire disc. Sweep, /// `Disc::patch` retry pass with `block_sectors >= 2`. `reverse=true` /// means walking bad ranges from highest to lowest LBA. Trim { reverse: bool }, /// `Disc::patch` final pass at 1 sector per block. Scrape { reverse: bool }, /// Demux ISO → output (MKV / M2TS / network). Single phase that runs /// after all rip passes complete. The library's mux pipeline does not /// currently emit `PassProgress` itself, so this variant exists for /// consumers (e.g. autorip) that label their own mux phase with the /// same `PassKind` vocabulary. Mux, /// Sector verification — reads every sector and classifies health. Verify, } /// One progress sample from a pipeline phase. /// /// `work_done / work_total` is the per-pass percentage — always 0..=100% /// regardless of which kind of pass is running. `bytes_good_total` is the /// cumulative count of confirmed-clean bytes across the whole rip; useful /// for the "data recovered" stat the user sees. /// /// For `PassKind::Verify`, the fields map as follows: /// - `work_done` = sectors read so far /// - `work_total` = total sectors in title /// - `bytes_good_total` = good + slow + recovered sectors × 2048 /// - `bytes_unreadable_total` = bad sectors × 2048 /// - `bytes_pending_total` = 0 (verify processes sequentially, nothing pending) #[derive(Debug, Clone, Copy)] pub struct PassProgress { pub kind: PassKind, pub work_done: u64, pub work_total: u64, pub bytes_good_total: u64, pub bytes_unreadable_total: u64, pub bytes_pending_total: u64, pub bytes_total_disc: u64, pub disc_duration_secs: Option, /// How many bytes of the worst-case damage (unreadable + pending) fall /// within the main title's extents. Zero means none of the damage /// affects the main movie — it's all in extras/menus. pub bytes_bad_in_main_title: u64, /// Main title duration in seconds. Same as disc_duration_secs when the /// disc has one dominant title, but separate so consumers can show both. pub main_title_duration_secs: Option, /// Main title size in bytes (sum of extent sizes). pub main_title_size_bytes: Option, } impl PassProgress { /// Percentage of work completed for this pass (0..=100). /// /// Returns `100.0` if `work_total` is zero to avoid division by zero. /// Clamped to `0..=100` so a transient `work_done > work_total` /// (e.g. a count that briefly overshoots) never reports above 100%. pub fn work_pct(&self) -> f64 { if self.work_total == 0 { return 100.0; } (self.work_done as f64 / self.work_total as f64 * 100.0).clamp(0.0, 100.0) } /// Percentage of the disc that is confirmed clean (0..=100). /// /// Computed from `bytes_good_total / bytes_total_disc`, clamped to /// `0..=100`. pub fn good_pct(&self) -> f64 { if self.bytes_total_disc == 0 { return 100.0; } (self.bytes_good_total as f64 / self.bytes_total_disc as f64 * 100.0).clamp(0.0, 100.0) } /// Percentage of the disc that is unreadable (0..=100). pub fn bad_pct(&self) -> f64 { if self.bytes_total_disc == 0 { return 0.0; } (self.bytes_unreadable_total as f64 / self.bytes_total_disc as f64 * 100.0) .clamp(0.0, 100.0) } /// Percentage of the disc that is still pending (not yet attempted or needs retry). pub fn pending_pct(&self) -> f64 { if self.bytes_total_disc == 0 { return 0.0; } (self.bytes_pending_total as f64 / self.bytes_total_disc as f64 * 100.0).clamp(0.0, 100.0) } } /// A consumer of pipeline progress events. Library code calls /// `Progress::report` once per inner-loop iteration (throttling is the /// consumer's job — `report` is cheap; the library doesn't gate it). /// /// Returns `true` to continue, `false` to request early stop. /// /// No `Send`/`Sync` bound — `report` is always called from the same thread /// running the pipeline, so closures with non-`Sync` captures (e.g. /// `RefCell`) work directly. Blanket impl below lets /// callers pass closures without explicit struct types. pub trait Progress { fn report(&self, p: &PassProgress) -> bool; } impl bool> Progress for F { fn report(&self, p: &PassProgress) -> bool { (self)(p) } }