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}