Add parser.rs

This commit is contained in:
2026-03-11 01:45:38 +08:00
parent ec68fe65bf
commit 78c79cddcd

167
src/parser.rs Normal file
View File

@@ -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<serde_json::Value>,
streams: Option<Vec<serde_json::Value>>,
}
/// Parse ffprobe JSON output into VideoMetadata
pub fn parse_ffprobe_json(json_str: &str, video_path: &str) -> Result<VideoMetadata> {
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<FormatInfo> {
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<VideoStream> {
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<AudioStream> {
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<SubtitleStream> {
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<OtherStream> {
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(),
})
}