diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..d9cf9c5 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,167 @@ +use chrono::Utc; +use serde::Deserialize; +use std::path::Path; +use crate::metadata::*; +use crate::error::{ProbeError, Result}; + +#[derive(Debug, Deserialize)] +struct FfprobeOutput { + format: Option, + streams: Option>, +} + +/// Parse ffprobe JSON output into VideoMetadata +pub fn parse_ffprobe_json(json_str: &str, video_path: &str) -> Result { + let ffprobe_data: FfprobeOutput = serde_json::from_str(json_str)?; + + let canonical_path = Path::new(video_path) + .canonicalize() + .map_err(|e| ProbeError::FileNotFound(format!("{}: {}", video_path, e)))?; + + let mut metadata = VideoMetadata { + video_path: canonical_path.to_string_lossy().to_string(), + probed_at: Utc::now(), + format: FormatInfo::default(), + video_stream: None, + audio_streams: Vec::new(), + subtitle_streams: Vec::new(), + other_streams: Vec::new(), + }; + + // Parse format + if let Some(fmt) = ffprobe_data.format { + metadata.format = parse_format(&fmt)?; + } + + // Parse streams + if let Some(streams) = ffprobe_data.streams { + for stream in streams { + let codec_type = stream.get("codec_type") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + match codec_type { + "video" if metadata.video_stream.is_none() => { + metadata.video_stream = Some(parse_video_stream(&stream)?); + } + "audio" => { + metadata.audio_streams.push(parse_audio_stream(&stream)?); + } + "subtitle" => { + metadata.subtitle_streams.push(parse_subtitle_stream(&stream)?); + } + _ => { + metadata.other_streams.push(parse_other_stream(&stream)?); + } + } + } + } + + Ok(metadata) +} + +fn parse_format(fmt: &serde_json::Value) -> Result { + Ok(FormatInfo { + filename: fmt.get("filename").and_then(|v| v.as_str()).map(String::from), + format_name: fmt.get("format_name").and_then(|v| v.as_str()).map(String::from), + format_long_name: fmt.get("format_long_name").and_then(|v| v.as_str()).map(String::from), + duration: fmt.get("duration") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0), + size: fmt.get("size") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()) + .unwrap_or(0), + bit_rate: fmt.get("bit_rate") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()) + .unwrap_or(0), + probe_score: fmt.get("probe_score") + .and_then(|v| v.as_i64()) + .map(|i| i as i32), + tags: fmt.get("tags").cloned(), + }) +} + +fn parse_video_stream(stream: &serde_json::Value) -> Result { + Ok(VideoStream { + index: stream.get("index") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + codec_name: stream.get("codec_name").and_then(|v| v.as_str()).map(String::from), + codec_long_name: stream.get("codec_long_name").and_then(|v| v.as_str()).map(String::from), + profile: stream.get("profile").and_then(|v| v.as_str()).map(String::from), + width: stream.get("width") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + height: stream.get("height") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + pix_fmt: stream.get("pix_fmt").and_then(|v| v.as_str()).map(String::from), + r_frame_rate: stream.get("r_frame_rate").and_then(|v| v.as_str()).map(String::from), + avg_frame_rate: stream.get("avg_frame_rate").and_then(|v| v.as_str()).map(String::from), + bit_rate: stream.get("bit_rate") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + duration: stream.get("duration") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + tags: stream.get("tags").cloned(), + }) +} + +fn parse_audio_stream(stream: &serde_json::Value) -> Result { + Ok(AudioStream { + index: stream.get("index") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + codec_name: stream.get("codec_name").and_then(|v| v.as_str()).map(String::from), + channels: stream.get("channels") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + sample_rate: stream.get("sample_rate").and_then(|v| v.as_str()).map(String::from), + bit_rate: stream.get("bit_rate") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + duration: stream.get("duration") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + tags: stream.get("tags").cloned(), + }) +} + +fn parse_subtitle_stream(stream: &serde_json::Value) -> Result { + Ok(SubtitleStream { + index: stream.get("index") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + codec_name: stream.get("codec_name").and_then(|v| v.as_str()).map(String::from), + language: stream.get("tags") + .and_then(|tags| tags.get("language")) + .and_then(|v| v.as_str()) + .map(String::from), + tags: stream.get("tags").cloned(), + }) +} + +fn parse_other_stream(stream: &serde_json::Value) -> Result { + Ok(OtherStream { + index: stream.get("index") + .and_then(|v| v.as_i64()) + .map(|i| i as i32) + .unwrap_or(0), + codec_type: stream.get("codec_type") + .and_then(|v| v.as_str()) + .map(String::from) + .unwrap_or_else(|| "unknown".to_string()), + codec_name: stream.get("codec_name").and_then(|v| v.as_str()).map(String::from), + tags: stream.get("tags").cloned(), + }) +} \ No newline at end of file