Skip to main content

aurora_modules/
media.rs

1use aurora_core::{AuroraResult, Pipeline, Value};
2use std::process::Command;
3
4fn find_tool(name: &str) -> bool {
5    Command::new("which")
6        .arg(name)
7        .output()
8        .ok()
9        .map(|o| o.status.success())
10        .unwrap_or(false)
11}
12
13fn check_tool(name: &str) -> AuroraResult<()> {
14    if !find_tool(name) {
15        return Err(aurora_core::AuroraError::CommandNotFound(
16            format!("{} is not installed", name)
17        ));
18    }
19    Ok(())
20}
21
22pub fn media_info(input: &str) -> AuroraResult<Pipeline> {
23    check_tool("ffprobe")?;
24    let output = Command::new("ffprobe")
25        .args(["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams"])
26        .arg(input)
27        .output()
28        .map_err(|e| aurora_core::AuroraError::ModuleError(
29            format!("failed to run ffprobe: {}", e)
30        ))?;
31
32    if !output.status.success() {
33        let stderr = String::from_utf8_lossy(&output.stderr);
34        return Err(aurora_core::AuroraError::ModuleError(
35            format!("ffprobe failed: {}", stderr)
36        ));
37    }
38
39    let raw = String::from_utf8_lossy(&output.stdout);
40    let parsed: serde_json::Value = serde_json::from_str(&raw)
41        .map_err(|e| aurora_core::AuroraError::ParseError(
42            format!("failed to parse ffprobe output: {}", e)
43        ))?;
44
45    let mut headers = vec![
46        "index".into(),
47        "codec".into(),
48        "type".into(),
49        "width".into(),
50        "height".into(),
51        "bitrate".into(),
52    ];
53    let mut rows: Vec<Vec<Value>> = Vec::new();
54
55    if let Some(streams) = parsed.get("streams").and_then(|v| v.as_array()) {
56        for s in streams {
57            let idx = s.get("index").and_then(|v| v.as_i64()).unwrap_or(0);
58            let codec = s.get("codec_name").and_then(|v| v.as_str()).unwrap_or("");
59            let kind = s.get("codec_type").and_then(|v| v.as_str()).unwrap_or("");
60            let w = s.get("width").and_then(|v| v.as_i64()).unwrap_or(0);
61            let h = s.get("height").and_then(|v| v.as_i64()).unwrap_or(0);
62            let br = s.get("bit_rate").and_then(|v| v.as_str()).unwrap_or("");
63
64            rows.push(vec![
65                Value::Int(idx),
66                Value::String(codec.into()),
67                Value::String(kind.into()),
68                Value::Int(w),
69                Value::Int(h),
70                Value::String(br.into()),
71            ]);
72        }
73    }
74
75    if let Some(format) = parsed.get("format") {
76        headers.push("format_name".into());
77        headers.push("duration".into());
78        headers.push("size".into());
79        if let Some(row) = rows.first_mut() {
80            let fmt_name = format.get("format_name").and_then(|v| v.as_str()).unwrap_or("");
81            let dur = format.get("duration").and_then(|v| v.as_str()).unwrap_or("");
82            let size = format.get("size").and_then(|v| v.as_str()).unwrap_or("");
83            row.push(Value::String(fmt_name.into()));
84            row.push(Value::String(dur.into()));
85            row.push(Value::String(size.into()));
86        } else {
87            let fmt_name = format.get("format_name").and_then(|v| v.as_str()).unwrap_or("");
88            let dur = format.get("duration").and_then(|v| v.as_str()).unwrap_or("");
89            let size = format.get("size").and_then(|v| v.as_str()).unwrap_or("");
90            rows.push(vec![
91                Value::Null, Value::Null, Value::Null, Value::Null, Value::Null, Value::Null,
92                Value::String(fmt_name.into()),
93                Value::String(dur.into()),
94                Value::String(size.into()),
95            ]);
96        }
97    }
98
99    Ok(Pipeline::table(headers, rows))
100}
101
102pub fn media_convert(input: &str, output: &str) -> AuroraResult<Pipeline> {
103    check_tool("ffmpeg")?;
104    let result = Command::new("ffmpeg")
105        .args(["-i", input])
106        .arg(output)
107        .output()
108        .map_err(|e| aurora_core::AuroraError::ModuleError(
109            format!("failed to run ffmpeg: {}", e)
110        ))?;
111
112    if !result.status.success() {
113        let stderr = String::from_utf8_lossy(&result.stderr);
114        return Err(aurora_core::AuroraError::ModuleError(
115            format!("ffmpeg conversion failed: {}", stderr)
116        ));
117    }
118
119    Ok(Pipeline::table(
120        vec!["action".into(), "input".into(), "output".into(), "status".into()],
121        vec![vec![
122            Value::String("convert".into()),
123            Value::String(input.into()),
124            Value::String(output.into()),
125            Value::String("ok".into()),
126        ]],
127    ))
128}
129
130pub fn media_stream(input: &str) -> AuroraResult<Pipeline> {
131    media_info(input)
132}
133
134pub fn media_extract(input: &str, output: &str) -> AuroraResult<Pipeline> {
135    check_tool("ffmpeg")?;
136    let result = Command::new("ffmpeg")
137        .args(["-i", input])
138        .arg(output)
139        .output()
140        .map_err(|e| aurora_core::AuroraError::ModuleError(
141            format!("failed to run ffmpeg: {}", e)
142        ))?;
143
144    if !result.status.success() {
145        let stderr = String::from_utf8_lossy(&result.stderr);
146        return Err(aurora_core::AuroraError::ModuleError(
147            format!("ffmpeg extraction failed: {}", stderr)
148        ));
149    }
150
151    Ok(Pipeline::table(
152        vec!["action".into(), "input".into(), "output".into(), "status".into()],
153        vec![vec![
154            Value::String("extract".into()),
155            Value::String(input.into()),
156            Value::String(output.into()),
157            Value::String("ok".into()),
158        ]],
159    ))
160}