use std::path::Path; use std::process::Command; use anyhow::{anyhow, Context, Result}; use serde::Deserialize; use crate::config::QualityTags; #[derive(Debug, Clone)] pub struct MediaInfo { pub duration_seconds: Option, pub height: Option, pub codec: Option, } #[derive(Debug, Deserialize)] struct FfprobeOutput { format: Option, streams: Option>, } #[derive(Debug, Deserialize)] struct FfprobeFormat { duration: Option, } #[derive(Debug, Deserialize)] struct FfprobeStream { codec_type: Option, codec_name: Option, height: Option, } pub fn probe(path: &Path) -> Result { let output = Command::new("ffprobe") .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() { return Err(anyhow!( "ffprobe failed for {}: {}", path.display(), String::from_utf8_lossy(&output.stderr) )); } let parsed: FfprobeOutput = serde_json::from_slice(&output.stdout) .with_context(|| "failed to parse ffprobe JSON")?; let duration_seconds = parsed .format .and_then(|fmt| fmt.duration) .and_then(|dur| dur.parse::().ok()); let video_stream = parsed .streams .unwrap_or_default() .into_iter() .find(|stream| stream.codec_type.as_deref() == Some("video")); let (height, codec) = if let Some(stream) = video_stream { (stream.height, stream.codec_name) } else { (None, None) }; Ok(MediaInfo { duration_seconds, height, codec, }) } pub fn quality_tag(info: &MediaInfo, tags: &QualityTags) -> Option { let mut parts: Vec = Vec::new(); if tags.resolution { if let Some(res) = resolution_tag(info.height) { parts.push(res); } } if tags.codec { if let Some(codec) = codec_tag(info.codec.as_deref()) { parts.push(codec); } } if tags.source { // Source tagging not implemented yet; placeholder for future expansion. } if parts.is_empty() { None } else { Some(parts.join(" ")) } } pub fn resolution_tag(height: Option) -> Option { let height = height?; let tag = if height >= 2160 { "2160p" } else if height >= 1080 { "1080p" } else if height >= 720 { "720p" } else if height >= 480 { "480p" } else { "360p" }; Some(tag.to_string()) } pub fn codec_tag(codec: Option<&str>) -> Option { let codec = codec?.to_ascii_lowercase(); let tag = if codec.contains("hevc") || codec.contains("h265") || codec.contains("x265") { "x265" } else if codec.contains("h264") || codec.contains("x264") { "x264" } else if codec.contains("av1") { "av1" } else { return None; }; Some(tag.to_string()) } #[cfg(test)] mod tests { use super::{codec_tag, resolution_tag}; #[test] fn resolution_tags() { assert_eq!(resolution_tag(Some(2160)).as_deref(), Some("2160p")); assert_eq!(resolution_tag(Some(1080)).as_deref(), Some("1080p")); assert_eq!(resolution_tag(Some(720)).as_deref(), Some("720p")); assert_eq!(resolution_tag(Some(480)).as_deref(), Some("480p")); assert_eq!(resolution_tag(Some(360)).as_deref(), Some("360p")); } #[test] fn codec_tags() { assert_eq!(codec_tag(Some("h264")).as_deref(), Some("x264")); assert_eq!(codec_tag(Some("hevc")).as_deref(), Some("x265")); assert_eq!(codec_tag(Some("av1")).as_deref(), Some("av1")); assert_eq!(codec_tag(Some("vp9")), None); } }