ffflow 0.1.1

FFmpeg workflow automation CLI/TUI with real-time progress tracking
use std::time::Duration;

use once_cell::sync::Lazy;
use regex::Regex;

#[derive(Debug, Clone, PartialEq)]
pub struct FfmpegProgress {
    pub frame: u64,
    pub fps: f32,
    pub time: Duration,
    pub bitrate_kbps: f32,
    pub speed: f32,
    pub size_bytes: u64,
}

static RE_FRAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"frame=\s*(\d+)").unwrap());
static RE_FPS: Lazy<Regex> = Lazy::new(|| Regex::new(r"fps=\s*([0-9]*\.?[0-9]+)").unwrap());
static RE_TIME: Lazy<Regex> = Lazy::new(|| Regex::new(r"time=\s*([0-9:\.]+)").unwrap());
static RE_BITRATE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"bitrate=\s*([0-9]*\.?[0-9]+)\s*([A-Za-z/]+)").unwrap());
static RE_SPEED: Lazy<Regex> = Lazy::new(|| Regex::new(r"speed=\s*([0-9]*\.?[0-9]+)x").unwrap());
static RE_SIZE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"size=\s*([0-9]*\.?[0-9]+)\s*([A-Za-z]+)").unwrap());

pub fn parse_progress_line(line: &str) -> Option<FfmpegProgress> {
    let frame = RE_FRAME
        .captures(line)
        .and_then(|cap| cap.get(1))
        .and_then(|m| m.as_str().parse::<u64>().ok());
    let fps = RE_FPS
        .captures(line)
        .and_then(|cap| cap.get(1))
        .and_then(|m| m.as_str().parse::<f32>().ok());
    let time = RE_TIME
        .captures(line)
        .and_then(|cap| cap.get(1))
        .and_then(|m| parse_ffmpeg_time(m.as_str()));
    let bitrate = RE_BITRATE.captures(line).and_then(|cap| {
        let value = cap.get(1)?.as_str().parse::<f32>().ok()?;
        let unit = cap.get(2)?.as_str();
        parse_bitrate_to_kbps(value, unit)
    });
    let speed = RE_SPEED
        .captures(line)
        .and_then(|cap| cap.get(1))
        .and_then(|m| m.as_str().parse::<f32>().ok());
    let size_bytes = RE_SIZE.captures(line).and_then(|cap| {
        let value = cap.get(1)?.as_str().parse::<f32>().ok()?;
        let unit = cap.get(2)?.as_str();
        parse_size_to_bytes(value, unit)
    });

    if frame.is_none()
        && fps.is_none()
        && time.is_none()
        && bitrate.is_none()
        && speed.is_none()
        && size_bytes.is_none()
    {
        return None;
    }

    Some(FfmpegProgress {
        frame: frame.unwrap_or(0),
        fps: fps.unwrap_or(0.0),
        time: time.unwrap_or(Duration::from_secs(0)),
        bitrate_kbps: bitrate.unwrap_or(0.0),
        speed: speed.unwrap_or(0.0),
        size_bytes: size_bytes.unwrap_or(0),
    })
}

pub fn parse_ffmpeg_time(value: &str) -> Option<Duration> {
    let parts: Vec<&str> = value.split(':').collect();
    if parts.is_empty() {
        return None;
    }

    let seconds = match parts.len() {
        1 => parts[0].parse::<f64>().ok()?,
        2 => {
            let minutes = parts[0].parse::<f64>().ok()?;
            let seconds = parts[1].parse::<f64>().ok()?;
            minutes * 60.0 + seconds
        }
        _ => {
            let hours = parts[0].parse::<f64>().ok()?;
            let minutes = parts[1].parse::<f64>().ok()?;
            let seconds = parts[2].parse::<f64>().ok()?;
            hours * 3600.0 + minutes * 60.0 + seconds
        }
    };

    let micros = (seconds * 1_000_000.0).round().max(0.0) as u64;
    Some(Duration::from_micros(micros))
}

pub fn parse_size_to_bytes(value: f32, unit: &str) -> Option<u64> {
    let unit = unit.trim().to_ascii_lowercase();
    let multiplier = match unit.as_str() {
        "b" | "bytes" => 1.0,
        "kb" => 1000.0,
        "kib" => 1024.0,
        "mb" => 1_000_000.0,
        "mib" => 1_048_576.0,
        "gb" => 1_000_000_000.0,
        "gib" => 1_073_741_824.0,
        "tb" => 1_000_000_000_000.0,
        "tib" => 1_099_511_627_776.0,
        _ => return None,
    };
    Some((value as f64 * multiplier).round().max(0.0) as u64)
}

pub fn parse_bitrate_to_kbps(value: f32, unit: &str) -> Option<f32> {
    let unit = unit.trim().to_ascii_lowercase();
    let multiplier = if unit.starts_with("kbit") {
        1.0
    } else if unit.starts_with("mbit") {
        1000.0
    } else if unit.starts_with("gbit") {
        1_000_000.0
    } else {
        return None;
    };
    Some(value * multiplier)
}