ffflow 0.1.1

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

use crate::core::metadata::{InputInfo, OutputInfo};
use crate::core::progress::FfmpegProgress;
use crate::core::summary::EncodeSummary;

pub fn format_input_line(info: &InputInfo) -> String {
    let resolution = if info.width > 0 && info.height > 0 {
        format!("{}x{}", info.width, info.height)
    } else {
        "unknown".to_string()
    };
    let fps = if info.fps > 0.0 {
        format!("{:.2}fps", info.fps)
    } else {
        "unknown fps".to_string()
    };
    let codec = if info.codec.is_empty() {
        "unknown".to_string()
    } else {
        info.codec.clone()
    };
    let container = info
        .container
        .clone()
        .unwrap_or_else(|| "unknown".to_string());
    let path = info.path.clone().unwrap_or_else(|| "unknown".to_string());
    let duration = info
        .duration
        .map(format_duration)
        .unwrap_or_else(|| "--:--:--".to_string());
    let bitrate = info
        .bitrate_kbps
        .map(|kbps| format!("{:.1} kb/s", kbps))
        .unwrap_or_else(|| "unknown".to_string());
    format!(
        "Input  : {path} ({container}/{codec} {resolution} @ {fps}, duration={duration}, bitrate={bitrate})"
    )
}

pub fn format_output_line(info: &OutputInfo) -> String {
    let resolution = if info.width > 0 && info.height > 0 {
        format!("{}x{}", info.width, info.height)
    } else {
        "unknown".to_string()
    };
    let codec = if info.codec.is_empty() {
        "unknown".to_string()
    } else {
        info.codec.clone()
    };
    let container = if info.container.is_empty() {
        "unknown".to_string()
    } else {
        info.container.clone()
    };
    let path = if info.path.is_empty() {
        "output".to_string()
    } else {
        info.path.clone()
    };
    format!("Output : {path} ({container}/{codec} {resolution})")
}

pub fn format_summary_line(summary: &EncodeSummary) -> String {
    let size = format_bytes(summary.final_size_bytes);
    let bitrate = if summary.avg_bitrate_kbps > 0.0 {
        format!("{:.1} kbps", summary.avg_bitrate_kbps)
    } else {
        "unknown".to_string()
    };
    let duration = format_duration(summary.duration);
    format!("Final  : size={size} avg_bitrate={bitrate} duration={duration}")
}

pub fn format_progress_line(update: &FfmpegProgress, total: Option<Duration>) -> Option<String> {
    if update.frame == 0 && update.speed == 0.0 && update.time == Duration::from_secs(0) {
        return None;
    }

    let elapsed = format_duration(update.time);
    let total = total
        .map(format_duration)
        .unwrap_or_else(|| "--:--:--".to_string());

    Some(format!(
        "progress: time={elapsed}/{total} frame={} speed={}x",
        update.frame,
        update.speed
    ))
}

pub fn format_duration(duration: Duration) -> String {
    let total_secs = duration.as_secs();
    let hours = total_secs / 3600;
    let minutes = (total_secs % 3600) / 60;
    let seconds = total_secs % 60;
    format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}

pub fn format_bytes(bytes: u64) -> String {
    const KB: f64 = 1024.0;
    const MB: f64 = KB * 1024.0;
    const GB: f64 = MB * 1024.0;
    let value = bytes as f64;
    if value >= GB {
        format!("{:.2} GB", value / GB)
    } else if value >= MB {
        format!("{:.2} MB", value / MB)
    } else if value >= KB {
        format!("{:.2} KB", value / KB)
    } else {
        format!("{} B", bytes)
    }
}