Initial vid-repair scaffold
This commit is contained in:
72
vid-repair-core/src/scan/decode.rs
Normal file
72
vid-repair-core/src/scan/decode.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::rules::RuleSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DecodeOutput {
|
||||
pub lines: Vec<String>,
|
||||
pub early_stop: bool,
|
||||
}
|
||||
|
||||
pub fn run_decode(path: &Path, ffmpeg_path: &str, ruleset: &RuleSet) -> Result<DecodeOutput> {
|
||||
let mut child = Command::new(ffmpeg_path)
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-i")
|
||||
.arg(path)
|
||||
.arg("-f")
|
||||
.arg("null")
|
||||
.arg("-")
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run ffmpeg decode for {}", path.display()))?;
|
||||
|
||||
let stderr = child.stderr.take().context("Failed to capture ffmpeg stderr")?;
|
||||
let reader = BufReader::new(stderr);
|
||||
|
||||
let early_stop = Arc::new(AtomicBool::new(false));
|
||||
let early_stop_flag = early_stop.clone();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line.unwrap_or_default();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(line.clone());
|
||||
|
||||
if should_stop(&line, ruleset) {
|
||||
early_stop_flag.store(true, Ordering::SeqCst);
|
||||
let _ = child.kill();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = child.wait();
|
||||
|
||||
Ok(DecodeOutput {
|
||||
lines,
|
||||
early_stop: early_stop.load(Ordering::SeqCst),
|
||||
})
|
||||
}
|
||||
|
||||
fn should_stop(line: &str, ruleset: &RuleSet) -> bool {
|
||||
for rule in &ruleset.rules {
|
||||
if !rule.rule.stop_scan {
|
||||
continue;
|
||||
}
|
||||
|
||||
if rule.patterns.iter().any(|re| re.is_match(line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
83
vid-repair-core/src/scan/ffprobe.rs
Normal file
83
vid-repair-core/src/scan/ffprobe.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::types::{ProbeData, StreamInfo};
|
||||
|
||||
pub fn run_ffprobe(path: &Path, ffprobe_path: &str) -> Result<ProbeData> {
|
||||
let output = Command::new(ffprobe_path)
|
||||
.arg("-v")
|
||||
.arg("error")
|
||||
.arg("-print_format")
|
||||
.arg("json")
|
||||
.arg("-show_format")
|
||||
.arg("-show_streams")
|
||||
.arg(path)
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run ffprobe on {}", path.display()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!(
|
||||
"ffprobe failed for {}: {}",
|
||||
path.display(),
|
||||
stderr.trim()
|
||||
);
|
||||
}
|
||||
|
||||
let raw: Value = serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| format!("Failed to parse ffprobe output for {}", path.display()))?;
|
||||
|
||||
let format_name = raw
|
||||
.get("format")
|
||||
.and_then(|fmt| fmt.get("format_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let duration = raw
|
||||
.get("format")
|
||||
.and_then(|fmt| fmt.get("duration"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<f64>().ok());
|
||||
|
||||
let streams = raw
|
||||
.get("streams")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.map(|stream| StreamInfo {
|
||||
codec_type: stream
|
||||
.get("codec_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
codec_name: stream
|
||||
.get("codec_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
width: stream.get("width").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
height: stream
|
||||
.get("height")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32),
|
||||
sample_rate: stream
|
||||
.get("sample_rate")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
channels: stream
|
||||
.get("channels")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|v| v as u32),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ProbeData {
|
||||
format_name,
|
||||
duration,
|
||||
streams,
|
||||
raw,
|
||||
})
|
||||
}
|
||||
48
vid-repair-core/src/scan/mod.rs
Normal file
48
vid-repair-core/src/scan/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::rules::{build_context, RuleMatch, RuleSet};
|
||||
|
||||
mod decode;
|
||||
mod ffprobe;
|
||||
mod types;
|
||||
|
||||
pub use types::{Issue, ProbeData, ScanOutcome, ScanRequest};
|
||||
|
||||
pub fn scan_file(path: &Path, config: &Config, ruleset: &RuleSet) -> Result<ScanOutcome> {
|
||||
let probe = ffprobe::run_ffprobe(path, &config.ffprobe_path)?;
|
||||
|
||||
let decode = decode::run_decode(path, &config.ffmpeg_path, ruleset)?;
|
||||
|
||||
let context = build_context(&probe);
|
||||
let matches = ruleset.match_lines(&decode.lines, &context);
|
||||
|
||||
let issues = matches
|
||||
.iter()
|
||||
.map(|hit| issue_from_match(hit))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(ScanOutcome {
|
||||
path: path.to_path_buf(),
|
||||
probe,
|
||||
issues,
|
||||
decode_errors: decode.lines,
|
||||
early_stop: decode.early_stop,
|
||||
})
|
||||
}
|
||||
|
||||
fn issue_from_match(hit: &RuleMatch) -> Issue {
|
||||
Issue {
|
||||
code: hit.rule_id.clone(),
|
||||
severity: hit.severity,
|
||||
fix_tier: hit.fix_tier,
|
||||
message: hit
|
||||
.notes
|
||||
.clone()
|
||||
.unwrap_or_else(|| hit.rule_id.clone()),
|
||||
evidence: hit.evidence.clone(),
|
||||
action: hit.action.clone(),
|
||||
}
|
||||
}
|
||||
47
vid-repair-core/src/scan/types.rs
Normal file
47
vid-repair-core/src/scan/types.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::rules::{FixTier, Severity};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamInfo {
|
||||
pub codec_type: Option<String>,
|
||||
pub codec_name: Option<String>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub sample_rate: Option<String>,
|
||||
pub channels: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProbeData {
|
||||
pub format_name: Option<String>,
|
||||
pub duration: Option<f64>,
|
||||
pub streams: Vec<StreamInfo>,
|
||||
pub raw: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Issue {
|
||||
pub code: String,
|
||||
pub severity: Severity,
|
||||
pub fix_tier: FixTier,
|
||||
pub message: String,
|
||||
pub evidence: Vec<String>,
|
||||
pub action: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ScanOutcome {
|
||||
pub path: PathBuf,
|
||||
pub probe: ProbeData,
|
||||
pub issues: Vec<Issue>,
|
||||
pub decode_errors: Vec<String>,
|
||||
pub early_stop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanRequest {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
Reference in New Issue
Block a user