Add parser.rs
This commit is contained in:
167
src/parser.rs
Normal file
167
src/parser.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user