ux: show fix phase progress and scan/plan output

This commit is contained in:
2026-01-01 17:29:19 -05:00
parent 36827c763f
commit 2e55677e35
2 changed files with 331 additions and 52 deletions

View File

@@ -1,15 +1,31 @@
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Stdio};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
use anyhow::{Context, Result};
use fs_err as fs;
use crate::config::Config;
use crate::fix::{FixKind, FixOutcome, FixPlan};
use crate::scan::scan_file;
use crate::scan::{scan_file, DecodeProgress};
use crate::rules::RuleSet;
pub fn apply_fix(path: &Path, plan: &FixPlan, config: &Config, ruleset: &RuleSet) -> Result<FixOutcome> {
apply_fix_with_progress(path, plan, config, ruleset, None, None)
}
pub fn apply_fix_with_progress(
path: &Path,
plan: &FixPlan,
config: &Config,
ruleset: &RuleSet,
duration: Option<f64>,
progress: Option<Arc<dyn Fn(DecodeProgress) + Send + Sync>>,
) -> Result<FixOutcome> {
if plan.actions.is_empty() {
return Ok(FixOutcome {
plan: plan.clone(),
@@ -27,7 +43,7 @@ pub fn apply_fix(path: &Path, plan: &FixPlan, config: &Config, ruleset: &RuleSet
let action = &plan.actions[0];
let output = prepare_output_path(path, config)?;
run_ffmpeg_fix(path, &output.temp_path, action.kind, config)?;
run_ffmpeg_fix(path, &output.temp_path, action.kind, config, duration, progress)?;
let verification = scan_file(&output.temp_path, config, ruleset)
.with_context(|| format!("Failed to verify output {}", output.temp_path.display()))?;
@@ -55,7 +71,14 @@ pub fn apply_fix(path: &Path, plan: &FixPlan, config: &Config, ruleset: &RuleSet
})
}
fn run_ffmpeg_fix(path: &Path, output: &Path, kind: FixKind, config: &Config) -> Result<()> {
fn run_ffmpeg_fix(
path: &Path,
output: &Path,
kind: FixKind,
config: &Config,
duration: Option<f64>,
progress: Option<Arc<dyn Fn(DecodeProgress) + Send + Sync>>,
) -> Result<()> {
let mut cmd = Command::new(&config.ffmpeg_path);
cmd.arg("-y").arg("-v").arg("error").arg("-i").arg(path);
@@ -76,13 +99,118 @@ fn run_ffmpeg_fix(path: &Path, output: &Path, kind: FixKind, config: &Config) ->
cmd.arg(output);
let output = cmd
.output()
if progress.is_none() {
let output = cmd
.output()
.with_context(|| format!("Failed to run ffmpeg fix for {}", path.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("ffmpeg fix failed: {}", stderr.trim());
}
return Ok(());
}
cmd.arg("-nostats").arg("-progress").arg("pipe:2");
cmd.stdout(Stdio::null()).stderr(Stdio::piped());
let mut child = cmd
.spawn()
.with_context(|| format!("Failed to run ffmpeg fix for {}", path.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("ffmpeg fix failed: {}", stderr.trim());
let stderr = child
.stderr
.take()
.context("Failed to capture ffmpeg stderr")?;
let reader = BufReader::new(stderr);
let progress_done = Arc::new(AtomicBool::new(false));
let progress_done_flag = progress_done.clone();
let snapshot = Arc::new(Mutex::new(ProgressSnapshot::default()));
let snapshot_thread = snapshot.clone();
let progress_cb = progress.clone().unwrap();
let mut out_time: Option<f64> = None;
let mut speed: Option<f64> = None;
let mut error_lines = Vec::new();
for line in reader.lines() {
let line = match line {
Ok(line) => line,
Err(_) => break,
};
if line.is_empty() {
continue;
}
if let Some((key, value)) = line.split_once('=') {
match key {
"out_time_ms" => {
out_time = value.parse::<f64>().ok().map(|ms| ms / 1_000_000.0);
}
"out_time_us" => {
out_time = value.parse::<f64>().ok().map(|us| us / 1_000_000.0);
}
"out_time" => {
out_time = parse_out_time(value).or(out_time);
}
"speed" => {
speed = parse_speed(value);
}
"progress" => {
let done = value == "end";
let percent = duration.and_then(|d| out_time.map(|t| (t / d * 100.0).min(100.0)));
if let Ok(mut snap) = snapshot_thread.lock() {
snap.out_time = out_time;
snap.speed = speed;
}
progress_cb(DecodeProgress {
out_time,
duration,
percent,
speed,
done,
});
if done {
progress_done_flag.store(true, Ordering::SeqCst);
}
}
_ => {}
}
} else {
error_lines.push(line);
}
}
let status = child
.wait()
.with_context(|| format!("Failed to wait for ffmpeg fix for {}", path.display()))?;
if !progress_done.load(Ordering::SeqCst) {
let snapshot = snapshot.lock().ok();
let out_time = snapshot.as_ref().and_then(|s| s.out_time);
let speed = snapshot.as_ref().and_then(|s| s.speed);
let percent = duration.map(|_| 100.0);
progress_cb(DecodeProgress {
out_time,
duration,
percent,
speed,
done: true,
});
}
if !status.success() {
let message = error_lines.join(" ");
anyhow::bail!(
"ffmpeg fix failed: {}",
if message.trim().is_empty() {
"unknown error"
} else {
message.trim()
}
);
}
Ok(())
@@ -208,3 +336,38 @@ fn next_original_path(path: &Path) -> Result<PathBuf> {
anyhow::bail!("Unable to find available .original name for {}", path.display());
}
#[derive(Default)]
struct ProgressSnapshot {
out_time: Option<f64>,
speed: Option<f64>,
}
fn parse_speed(value: &str) -> Option<f64> {
value.trim().trim_end_matches('x').parse::<f64>().ok()
}
fn parse_out_time(value: &str) -> Option<f64> {
let trimmed = value.trim();
if trimmed.is_empty() {
return None;
}
if let Ok(seconds) = trimmed.parse::<f64>() {
return Some(seconds);
}
let parts: Vec<&str> = trimmed.split(':').collect();
if parts.len() == 3 {
let hours = parts[0].parse::<f64>().ok()?;
let minutes = parts[1].parse::<f64>().ok()?;
let seconds = parts[2].parse::<f64>().ok()?;
return Some(hours * 3600.0 + minutes * 60.0 + seconds);
}
if parts.len() == 2 {
let minutes = parts[0].parse::<f64>().ok()?;
let seconds = parts[1].parse::<f64>().ok()?;
return Some(minutes * 60.0 + seconds);
}
None
}