feat: add quiet/verbose modes with command logging
This commit is contained in:
10
README.md
10
README.md
@@ -49,6 +49,16 @@ vid-repair fix --policy aggressive /path/to/videos
|
||||
vid-repair scan --json /path/to/videos
|
||||
```
|
||||
|
||||
### Verbosity 🔈
|
||||
|
||||
```bash
|
||||
# More detail (commands + error lines)
|
||||
vid-repair fix -v /path/to/videos
|
||||
|
||||
# Less detail (summary only)
|
||||
vid-repair scan -q /path/to/videos
|
||||
```
|
||||
|
||||
**Note:** `fix` always performs a full scan first, then repairs only if issues are found.
|
||||
Use `scan` when you just want a report without making changes.
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ keep_original = false
|
||||
json = false
|
||||
# Pretty-print JSON output.
|
||||
pretty = true
|
||||
# quiet|normal|verbose - controls console verbosity.
|
||||
verbosity = "normal"
|
||||
|
||||
[performance]
|
||||
# 0 = auto (num CPU threads). Higher values spawn more concurrent scans.
|
||||
|
||||
@@ -129,6 +129,8 @@ pub struct ReportConfig {
|
||||
pub json: bool,
|
||||
#[serde(default)]
|
||||
pub pretty: bool,
|
||||
#[serde(default)]
|
||||
pub verbosity: Verbosity,
|
||||
}
|
||||
|
||||
impl Default for ReportConfig {
|
||||
@@ -136,10 +138,25 @@ impl Default for ReportConfig {
|
||||
Self {
|
||||
json: false,
|
||||
pretty: true,
|
||||
verbosity: Verbosity::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Verbosity {
|
||||
Quiet,
|
||||
Normal,
|
||||
Verbose,
|
||||
}
|
||||
|
||||
impl Default for Verbosity {
|
||||
fn default() -> Self {
|
||||
Verbosity::Normal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceConfig {
|
||||
#[serde(default)]
|
||||
@@ -185,6 +202,7 @@ pub struct ConfigOverrides {
|
||||
pub output_dir: Option<String>,
|
||||
pub keep_original: Option<bool>,
|
||||
pub json: Option<bool>,
|
||||
pub verbosity: Option<Verbosity>,
|
||||
pub jobs: Option<usize>,
|
||||
pub watch: Option<bool>,
|
||||
}
|
||||
@@ -215,6 +233,9 @@ impl Config {
|
||||
if let Some(value) = overrides.json {
|
||||
self.report.json = value;
|
||||
}
|
||||
if let Some(value) = overrides.verbosity {
|
||||
self.report.verbosity = value;
|
||||
}
|
||||
if let Some(value) = overrides.jobs {
|
||||
self.performance.jobs = value;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ 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)
|
||||
apply_fix_with_progress(path, plan, config, ruleset, None, None, None)
|
||||
}
|
||||
|
||||
pub fn apply_fix_with_progress(
|
||||
@@ -25,6 +25,7 @@ pub fn apply_fix_with_progress(
|
||||
ruleset: &RuleSet,
|
||||
duration: Option<f64>,
|
||||
progress: Option<Arc<dyn Fn(DecodeProgress) + Send + Sync>>,
|
||||
log_cmd: Option<Arc<dyn Fn(String) + Send + Sync>>,
|
||||
) -> Result<FixOutcome> {
|
||||
if plan.actions.is_empty() {
|
||||
return Ok(FixOutcome {
|
||||
@@ -43,7 +44,15 @@ pub fn apply_fix_with_progress(
|
||||
let action = &plan.actions[0];
|
||||
let output = prepare_output_path(path, config)?;
|
||||
|
||||
run_ffmpeg_fix(path, &output.temp_path, action.kind, config, duration, progress)?;
|
||||
run_ffmpeg_fix(
|
||||
path,
|
||||
&output.temp_path,
|
||||
action.kind,
|
||||
config,
|
||||
duration,
|
||||
progress,
|
||||
log_cmd,
|
||||
)?;
|
||||
|
||||
let verification = scan_file(&output.temp_path, config, ruleset)
|
||||
.with_context(|| format!("Failed to verify output {}", output.temp_path.display()))?;
|
||||
@@ -78,26 +87,44 @@ fn run_ffmpeg_fix(
|
||||
config: &Config,
|
||||
duration: Option<f64>,
|
||||
progress: Option<Arc<dyn Fn(DecodeProgress) + Send + Sync>>,
|
||||
log_cmd: Option<Arc<dyn Fn(String) + Send + Sync>>,
|
||||
) -> Result<()> {
|
||||
let mut cmd = Command::new(&config.ffmpeg_path);
|
||||
cmd.arg("-y").arg("-v").arg("error").arg("-i").arg(path);
|
||||
let mut args = vec![
|
||||
"-y".to_string(),
|
||||
"-v".to_string(),
|
||||
"error".to_string(),
|
||||
"-i".to_string(),
|
||||
path.display().to_string(),
|
||||
];
|
||||
|
||||
match kind {
|
||||
FixKind::Remux => {
|
||||
cmd.arg("-c").arg("copy");
|
||||
args.extend(["-c", "copy"].iter().map(|s| s.to_string()));
|
||||
}
|
||||
FixKind::Faststart => {
|
||||
cmd.arg("-c").arg("copy");
|
||||
cmd.arg("-movflags").arg("+faststart");
|
||||
args.extend(["-c", "copy", "-movflags", "+faststart"].iter().map(|s| s.to_string()));
|
||||
}
|
||||
FixKind::Reencode => {
|
||||
cmd.arg("-c:v").arg("libx264");
|
||||
cmd.arg("-c:a").arg("aac");
|
||||
cmd.arg("-movflags").arg("+faststart");
|
||||
args.extend(
|
||||
["-c:v", "libx264", "-c:a", "aac", "-movflags", "+faststart"]
|
||||
.iter()
|
||||
.map(|s| s.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.arg(output);
|
||||
if progress.is_some() {
|
||||
args.extend(["-nostats", "-progress", "pipe:2"].iter().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
args.push(output.display().to_string());
|
||||
|
||||
if let Some(log_cmd) = log_cmd {
|
||||
log_cmd(format_command_line(&config.ffmpeg_path, &args));
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&config.ffmpeg_path);
|
||||
cmd.args(&args);
|
||||
|
||||
if progress.is_none() {
|
||||
let output = cmd
|
||||
@@ -112,7 +139,6 @@ fn run_ffmpeg_fix(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
cmd.arg("-nostats").arg("-progress").arg("pipe:2");
|
||||
cmd.stdout(Stdio::null()).stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd
|
||||
@@ -371,3 +397,25 @@ fn parse_out_time(value: &str) -> Option<f64> {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn format_command_line(program: &str, args: &[String]) -> String {
|
||||
let mut line = String::new();
|
||||
line.push_str("e_arg(program));
|
||||
for arg in args {
|
||||
line.push(' ');
|
||||
line.push_str("e_arg(arg));
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
fn quote_arg(arg: &str) -> String {
|
||||
if arg.is_empty() {
|
||||
return "\"\"".to_string();
|
||||
}
|
||||
if arg.chars().any(|c| c.is_whitespace() || c == '"' || c == '\'') {
|
||||
let escaped = arg.replace('"', "\\\"");
|
||||
format!("\"{}\"", escaped)
|
||||
} else {
|
||||
arg.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use clap::{Parser, Subcommand, ValueEnum};
|
||||
use rayon::prelude::*;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
|
||||
use vid_repair_core::config::{Config, ConfigOverrides, FixPolicy, ScanDepth};
|
||||
use vid_repair_core::config::{Config, ConfigOverrides, FixPolicy, ScanDepth, Verbosity};
|
||||
use vid_repair_core::fix::{self, FixOutcome, FixPlan};
|
||||
use vid_repair_core::report::{
|
||||
render_fix_line, render_json, render_scan_line, render_summary, FixJsonReport,
|
||||
@@ -30,6 +30,14 @@ struct Cli {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
/// Increase verbosity (show commands and error details)
|
||||
#[arg(short = 'v', long, action = clap::ArgAction::Count, conflicts_with = "quiet")]
|
||||
verbose: u8,
|
||||
|
||||
/// Reduce output (hide progress/details)
|
||||
#[arg(short = 'q', long, conflicts_with = "verbose")]
|
||||
quiet: bool,
|
||||
|
||||
/// Number of parallel jobs (0 = auto)
|
||||
#[arg(long)]
|
||||
jobs: Option<usize>,
|
||||
@@ -50,6 +58,8 @@ struct Cli {
|
||||
struct CommonArgs {
|
||||
config: Option<PathBuf>,
|
||||
json: bool,
|
||||
verbose: u8,
|
||||
quiet: bool,
|
||||
jobs: Option<usize>,
|
||||
ffmpeg_path: Option<String>,
|
||||
ffprobe_path: Option<String>,
|
||||
@@ -200,6 +210,8 @@ fn run() -> Result<i32> {
|
||||
let Cli {
|
||||
config,
|
||||
json,
|
||||
verbose,
|
||||
quiet,
|
||||
jobs,
|
||||
ffmpeg_path,
|
||||
ffprobe_path,
|
||||
@@ -209,6 +221,8 @@ fn run() -> Result<i32> {
|
||||
let common = CommonArgs {
|
||||
config,
|
||||
json,
|
||||
verbose,
|
||||
quiet,
|
||||
jobs,
|
||||
ffmpeg_path,
|
||||
ffprobe_path,
|
||||
@@ -266,6 +280,11 @@ fn handle_scan(args: ScanArgs, common: &CommonArgs) -> Result<i32> {
|
||||
if common.json {
|
||||
overrides.json = Some(true);
|
||||
}
|
||||
if common.quiet {
|
||||
overrides.verbosity = Some(Verbosity::Quiet);
|
||||
} else if common.verbose > 0 {
|
||||
overrides.verbosity = Some(Verbosity::Verbose);
|
||||
}
|
||||
if let Some(jobs) = common.jobs {
|
||||
overrides.jobs = Some(jobs);
|
||||
}
|
||||
@@ -298,7 +317,7 @@ fn handle_scan(args: ScanArgs, common: &CommonArgs) -> Result<i32> {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if !config.report.json {
|
||||
if show_phase(&config) {
|
||||
eprintln!("Scanning {} file(s)...", files.len());
|
||||
}
|
||||
|
||||
@@ -314,9 +333,11 @@ fn handle_scan(args: ScanArgs, common: &CommonArgs) -> Result<i32> {
|
||||
let json = render_json(&payload, config.report.pretty)?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
if !is_quiet(&config) {
|
||||
for scan in &scans {
|
||||
println!("{}", render_scan_line(scan));
|
||||
}
|
||||
}
|
||||
println!("{}", render_summary(&scans, None));
|
||||
}
|
||||
|
||||
@@ -345,6 +366,11 @@ fn handle_fix(args: FixArgs, common: &CommonArgs) -> Result<i32> {
|
||||
if common.json {
|
||||
overrides.json = Some(true);
|
||||
}
|
||||
if common.quiet {
|
||||
overrides.verbosity = Some(Verbosity::Quiet);
|
||||
} else if common.verbose > 0 {
|
||||
overrides.verbosity = Some(Verbosity::Verbose);
|
||||
}
|
||||
overrides.jobs = common.jobs;
|
||||
overrides.scan_depth = args.scan_depth.map(ScanDepth::from);
|
||||
if args.recursive {
|
||||
@@ -378,7 +404,7 @@ fn handle_fix(args: FixArgs, common: &CommonArgs) -> Result<i32> {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if !config.report.json {
|
||||
if show_phase(&config) {
|
||||
if args.dry_run {
|
||||
eprintln!("Planning fixes for {} file(s)...", files.len());
|
||||
} else {
|
||||
@@ -399,9 +425,11 @@ fn handle_fix(args: FixArgs, common: &CommonArgs) -> Result<i32> {
|
||||
let json = render_json(&payload, config.report.pretty)?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
if !is_quiet(&config) {
|
||||
for (scan, fix) in scans.iter().zip(fixes.iter()) {
|
||||
println!("{}", render_fix_line(scan, fix));
|
||||
}
|
||||
}
|
||||
println!("{}", render_summary(&scans, Some(&fixes)));
|
||||
}
|
||||
|
||||
@@ -433,7 +461,7 @@ fn run_scans(files: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<
|
||||
let errors = AtomicUsize::new(0);
|
||||
let started = AtomicUsize::new(0);
|
||||
let total = files.len();
|
||||
let show_progress = !config.report.json && std::io::stderr().is_terminal();
|
||||
let show_progress = progress_enabled(config);
|
||||
let in_place = show_progress && jobs.is_none();
|
||||
|
||||
let scans = if let Some(jobs) = jobs {
|
||||
@@ -449,13 +477,25 @@ fn run_scans(files: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<
|
||||
eprint!("[SCAN {}/{}] {}", idx, total, path.display());
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", idx, total, path, in_place))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match scan_file_with_progress(path, config, ruleset, progress) {
|
||||
Ok(scan) => Some(scan),
|
||||
Ok(scan) => {
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
Some(scan)
|
||||
}
|
||||
Err(err) => {
|
||||
if in_place {
|
||||
eprintln!();
|
||||
@@ -479,13 +519,25 @@ fn run_scans(files: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<
|
||||
eprint!("[SCAN {}/{}] {}", idx, total, path.display());
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", idx, total, path, in_place))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match scan_file_with_progress(path, config, ruleset, progress) {
|
||||
Ok(scan) => Some(scan),
|
||||
Ok(scan) => {
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
Some(scan)
|
||||
}
|
||||
Err(err) => {
|
||||
if in_place {
|
||||
eprintln!();
|
||||
@@ -535,10 +587,16 @@ fn process_fix_batch(
|
||||
let mut fixes = Vec::new();
|
||||
let mut errors = 0usize;
|
||||
let total = files.len();
|
||||
let show_progress = !config.report.json && std::io::stderr().is_terminal();
|
||||
let show_progress = progress_enabled(config);
|
||||
let in_place = show_progress;
|
||||
let mut idx = 0usize;
|
||||
|
||||
let log_cmd: Option<Arc<dyn Fn(String) + Send + Sync>> = if is_verbose(config) {
|
||||
Some(Arc::new(|cmd: String| eprintln!("[CMD] {}", cmd)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for path in files {
|
||||
idx += 1;
|
||||
if show_progress && !in_place {
|
||||
@@ -547,6 +605,13 @@ fn process_fix_batch(
|
||||
eprint!("[SCAN {}/{}] {}", idx, total, path.display());
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, &path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, &path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let scan_progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", idx, total, &path, in_place))
|
||||
} else {
|
||||
@@ -563,11 +628,14 @@ fn process_fix_batch(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if show_progress {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_scan_result_line(&scan));
|
||||
}
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
if show_progress {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_plan_line(&plan));
|
||||
}
|
||||
let outcome = if dry_run {
|
||||
@@ -587,6 +655,7 @@ fn process_fix_batch(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let log_cmd = log_cmd.clone();
|
||||
match fix::executor::apply_fix_with_progress(
|
||||
&path,
|
||||
&plan,
|
||||
@@ -594,6 +663,7 @@ fn process_fix_batch(
|
||||
ruleset,
|
||||
scan.probe.duration,
|
||||
fix_progress,
|
||||
log_cmd,
|
||||
) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => FixOutcome {
|
||||
@@ -623,14 +693,28 @@ fn process_fix_batch_parallel(
|
||||
let errors = AtomicUsize::new(0);
|
||||
let started = AtomicUsize::new(0);
|
||||
let total = files.len();
|
||||
let show_progress = !config.report.json && std::io::stderr().is_terminal();
|
||||
let show_progress = progress_enabled(config);
|
||||
let log_cmd: Option<Arc<dyn Fn(String) + Send + Sync>> = if is_verbose(config) {
|
||||
Some(Arc::new(|cmd: String| eprintln!("[CMD] {}", cmd)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let results = files
|
||||
.par_iter()
|
||||
.filter_map(|path| {
|
||||
let log_cmd = log_cmd.clone();
|
||||
let idx = started.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
if show_progress {
|
||||
eprintln!("[SCAN {}/{}] {}", idx, total, path.display());
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let scan_progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", idx, total, path, false))
|
||||
} else {
|
||||
@@ -644,11 +728,14 @@ fn process_fix_batch_parallel(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if show_progress {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_scan_result_line(&scan));
|
||||
}
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
if show_progress {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_plan_line(&plan));
|
||||
}
|
||||
let outcome = if dry_run {
|
||||
@@ -672,6 +759,7 @@ fn process_fix_batch_parallel(
|
||||
ruleset,
|
||||
scan.probe.duration,
|
||||
fix_progress,
|
||||
log_cmd,
|
||||
) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => FixOutcome {
|
||||
@@ -696,7 +784,7 @@ fn process_fix_batch_parallel(
|
||||
fn watch_scan(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result<()> {
|
||||
println!("Watch mode enabled. Waiting for files to settle...");
|
||||
watch::watch_paths(&paths, config, |path| {
|
||||
let show_progress = !config.report.json && std::io::stderr().is_terminal();
|
||||
let show_progress = progress_enabled(config);
|
||||
let in_place = show_progress;
|
||||
if show_progress && !in_place {
|
||||
eprintln!("[SCAN] {}", path.display());
|
||||
@@ -704,6 +792,13 @@ fn watch_scan(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result
|
||||
eprint!("[SCAN] {}", path.display());
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, &path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, &path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let scan_progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", 1, 1, &path, in_place))
|
||||
} else {
|
||||
@@ -711,6 +806,12 @@ fn watch_scan(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result
|
||||
};
|
||||
match scan_file_with_progress(&path, config, ruleset, scan_progress) {
|
||||
Ok(scan) => {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_scan_result_line(&scan));
|
||||
}
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
println!("{}", render_scan_line(&scan));
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -722,8 +823,13 @@ fn watch_scan(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet) -> Result
|
||||
|
||||
fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: bool) -> Result<()> {
|
||||
println!("Watch mode enabled. Waiting for files to settle...");
|
||||
let log_cmd: Option<Arc<dyn Fn(String) + Send + Sync>> = if is_verbose(config) {
|
||||
Some(Arc::new(|cmd: String| eprintln!("[CMD] {}", cmd)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
watch::watch_paths(&paths, config, |path| {
|
||||
let show_progress = !config.report.json && std::io::stderr().is_terminal();
|
||||
let show_progress = progress_enabled(config);
|
||||
let in_place = show_progress;
|
||||
if show_progress && !in_place {
|
||||
eprintln!("[SCAN] {}", path.display());
|
||||
@@ -731,6 +837,13 @@ fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: b
|
||||
eprint!("[SCAN] {}", path.display());
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
if is_verbose(config) {
|
||||
eprintln!("{}", render_ffprobe_cmd(&config.ffprobe_path, &path));
|
||||
eprintln!(
|
||||
"{}",
|
||||
render_decode_cmd(&config.ffmpeg_path, &path, config.scan.depth, show_progress)
|
||||
);
|
||||
}
|
||||
let scan_progress = if show_progress {
|
||||
Some(make_progress_callback("SCAN", 1, 1, &path, in_place))
|
||||
} else {
|
||||
@@ -739,10 +852,13 @@ fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: b
|
||||
match scan_file_with_progress(&path, config, ruleset, scan_progress) {
|
||||
Ok(scan) => {
|
||||
let plan = fix::planner::plan_fix(&scan.issues, config.repair.policy);
|
||||
if show_progress {
|
||||
if show_phase(config) {
|
||||
eprintln!("{}", render_scan_result_line(&scan));
|
||||
eprintln!("{}", render_plan_line(&plan));
|
||||
}
|
||||
if is_verbose(config) {
|
||||
emit_verbose_scan_details(config, &scan);
|
||||
}
|
||||
let outcome = if dry_run {
|
||||
fix::planner::plan_outcome(plan)
|
||||
} else {
|
||||
@@ -760,6 +876,7 @@ fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: b
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let log_cmd = log_cmd.clone();
|
||||
match fix::executor::apply_fix_with_progress(
|
||||
&path,
|
||||
&plan,
|
||||
@@ -767,6 +884,7 @@ fn watch_fix(paths: Vec<PathBuf>, config: &Config, ruleset: &RuleSet, dry_run: b
|
||||
ruleset,
|
||||
scan.probe.duration,
|
||||
fix_progress,
|
||||
log_cmd,
|
||||
) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(err) => {
|
||||
@@ -926,6 +1044,113 @@ fn render_plan_line(plan: &FixPlan) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_quiet(config: &Config) -> bool {
|
||||
matches!(config.report.verbosity, Verbosity::Quiet)
|
||||
}
|
||||
|
||||
fn is_verbose(config: &Config) -> bool {
|
||||
matches!(config.report.verbosity, Verbosity::Verbose)
|
||||
}
|
||||
|
||||
fn show_phase(config: &Config) -> bool {
|
||||
!config.report.json && !is_quiet(config)
|
||||
}
|
||||
|
||||
fn progress_enabled(config: &Config) -> bool {
|
||||
show_phase(config) && std::io::stderr().is_terminal()
|
||||
}
|
||||
|
||||
fn emit_verbose_scan_details(config: &Config, scan: &ScanOutcome) {
|
||||
if !is_verbose(config) || config.report.json {
|
||||
return;
|
||||
}
|
||||
|
||||
if !scan.decode_errors.is_empty() {
|
||||
eprintln!(
|
||||
"[DECODE] {} error line(s)",
|
||||
scan.decode_errors.len()
|
||||
);
|
||||
for line in &scan.decode_errors {
|
||||
eprintln!(" {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
if !scan.issues.is_empty() {
|
||||
eprintln!("[ISSUES] {} total", scan.issues.len());
|
||||
for issue in &scan.issues {
|
||||
eprintln!(
|
||||
" - {} [{:?}] {}",
|
||||
issue.code, issue.severity, issue.message
|
||||
);
|
||||
for evidence in &issue.evidence {
|
||||
eprintln!(" > {}", evidence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_ffprobe_cmd(ffprobe_path: &str, path: &Path) -> String {
|
||||
let args = vec![
|
||||
"-v".to_string(),
|
||||
"error".to_string(),
|
||||
"-print_format".to_string(),
|
||||
"json".to_string(),
|
||||
"-show_format".to_string(),
|
||||
"-show_streams".to_string(),
|
||||
path.display().to_string(),
|
||||
];
|
||||
format!("[CMD] {}", format_command_line(ffprobe_path, &args))
|
||||
}
|
||||
|
||||
fn render_decode_cmd(
|
||||
ffmpeg_path: &str,
|
||||
path: &Path,
|
||||
depth: ScanDepth,
|
||||
with_progress: bool,
|
||||
) -> String {
|
||||
let mut args = vec![
|
||||
"-v".to_string(),
|
||||
"error".to_string(),
|
||||
"-i".to_string(),
|
||||
path.display().to_string(),
|
||||
];
|
||||
match depth {
|
||||
ScanDepth::Quick => args.extend(["-t", "5"].iter().map(|s| s.to_string())),
|
||||
ScanDepth::Standard => args.extend(["-t", "30"].iter().map(|s| s.to_string())),
|
||||
ScanDepth::Deep => {}
|
||||
}
|
||||
|
||||
if with_progress {
|
||||
args.extend(["-nostats", "-progress", "pipe:1"].iter().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
args.extend(["-f", "null", "-"].iter().map(|s| s.to_string()));
|
||||
|
||||
format!("[CMD] {}", format_command_line(ffmpeg_path, &args))
|
||||
}
|
||||
|
||||
fn format_command_line(program: &str, args: &[String]) -> String {
|
||||
let mut line = String::new();
|
||||
line.push_str("e_arg(program));
|
||||
for arg in args {
|
||||
line.push(' ');
|
||||
line.push_str("e_arg(arg));
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
fn quote_arg(arg: &str) -> String {
|
||||
if arg.is_empty() {
|
||||
return "\"\"".to_string();
|
||||
}
|
||||
if arg.chars().any(|c| c.is_whitespace() || c == '"' || c == '\'') {
|
||||
let escaped = arg.replace('"', "\\\"");
|
||||
format!("\"{}\"", escaped)
|
||||
} else {
|
||||
arg.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_progress_line(line: &str, done: bool, in_place: bool, state: Arc<Mutex<ProgressState>>) {
|
||||
if !in_place {
|
||||
eprintln!("{}", line);
|
||||
|
||||
Reference in New Issue
Block a user