1use crate::core::hls_downloader::HlsDownloadResult;
2use tokio_util::sync::CancellationToken;
3
4pub struct HlsDownloadOptions<'a> {
5 pub m3u8_url: &'a str,
6 pub output: &'a str,
7 pub referer: &'a str,
8 pub bytes_tx: Option<tokio::sync::mpsc::UnboundedSender<u64>>,
9 pub cancel_token: CancellationToken,
10 pub max_concurrent: u32,
11 pub max_retries: u32,
12 pub client: Option<reqwest::Client>,
13 pub max_height: Option<u32>,
14}
15
16pub struct MediaProcessor;
17
18impl MediaProcessor {
19 pub async fn download_hls(
20 options: HlsDownloadOptions<'_>,
21 ) -> anyhow::Result<HlsDownloadResult> {
22 let downloader = match options.client {
23 Some(c) => crate::core::hls_downloader::HlsDownloader::with_client(c),
24 None => crate::core::hls_downloader::HlsDownloader::new(),
25 };
26 downloader
27 .download(
28 options.m3u8_url,
29 options.output,
30 options.referer,
31 options.bytes_tx,
32 options.cancel_token,
33 options.max_concurrent,
34 options.max_retries,
35 )
36 .await
37 }
38
39 pub async fn download_hls_with_quality(
40 options: HlsDownloadOptions<'_>,
41 ) -> anyhow::Result<HlsDownloadResult> {
42 let downloader = match options.client {
43 Some(c) => crate::core::hls_downloader::HlsDownloader::with_client(c),
44 None => crate::core::hls_downloader::HlsDownloader::new(),
45 };
46 downloader
47 .download_with_quality(
48 options.m3u8_url,
49 options.output,
50 options.referer,
51 options.bytes_tx,
52 options.cancel_token,
53 options.max_concurrent,
54 options.max_retries,
55 options.max_height,
56 )
57 .await
58 }
59
60 pub async fn remux(input: &str, output: &str) -> anyhow::Result<()> {
61 let status = crate::core::process::command("ffmpeg")
62 .args(["-y", "-i", input, "-c", "copy", output])
63 .stdout(std::process::Stdio::null())
64 .stderr(std::process::Stdio::piped())
65 .status()
66 .await?;
67
68 if !status.success() {
69 anyhow::bail!("FFmpeg remux falhou com status {}", status);
70 }
71
72 Ok(())
73 }
74
75 pub async fn merge_audio_video(video: &str, audio: &str, output: &str) -> anyhow::Result<()> {
76 let status = crate::core::process::command("ffmpeg")
77 .args([
78 "-y", "-i", video, "-i", audio, "-map", "0:v", "-map", "1:a", "-c", "copy", output,
79 ])
80 .stdout(std::process::Stdio::null())
81 .stderr(std::process::Stdio::piped())
82 .status()
83 .await?;
84
85 if !status.success() {
86 anyhow::bail!("FFmpeg merge falhou com status {}", status);
87 }
88
89 Ok(())
90 }
91
92 pub async fn download_direct(
93 url: &str,
94 output: &str,
95 headers: &[(&str, &str)],
96 ) -> anyhow::Result<()> {
97 let mut args = vec!["-y".to_string()];
98
99 if !headers.is_empty() {
100 let header_str: String = headers
101 .iter()
102 .map(|(k, v)| format!("{}: {}\r\n", k, v))
103 .collect();
104 args.extend(["-headers".to_string(), header_str]);
105 }
106
107 args.extend([
108 "-i".to_string(),
109 url.to_string(),
110 "-c".to_string(),
111 "copy".to_string(),
112 output.to_string(),
113 ]);
114
115 let status = crate::core::process::command("ffmpeg")
116 .args(&args)
117 .stdout(std::process::Stdio::null())
118 .stderr(std::process::Stdio::piped())
119 .status()
120 .await?;
121
122 if !status.success() {
123 anyhow::bail!("FFmpeg download_direct falhou com status {}", status);
124 }
125
126 Ok(())
127 }
128}
129
130pub trait CommandRunner {
131 fn check_command_success(&self, program: &str, args: &[&str]) -> bool;
132}
133
134pub struct RealCommandRunner;
135
136impl CommandRunner for RealCommandRunner {
137 fn check_command_success(&self, program: &str, args: &[&str]) -> bool {
138 crate::core::process::std_command(program)
139 .args(args)
140 .stdout(std::process::Stdio::null())
141 .stderr(std::process::Stdio::null())
142 .status()
143 .map(|s| s.success())
144 .unwrap_or(false)
145 }
146}
147
148pub fn check_ffmpeg_with_runner(runner: &impl CommandRunner) -> bool {
149 runner.check_command_success("ffmpeg", &["-version"])
150}
151
152pub fn check_ffmpeg() -> bool {
153 check_ffmpeg_with_runner(&RealCommandRunner)
154}
155
156pub fn check_ytdlp_with_runner(runner: &impl CommandRunner) -> bool {
157 runner.check_command_success("yt-dlp", &["--version"])
158}
159
160pub fn check_ytdlp() -> bool {
161 check_ytdlp_with_runner(&RealCommandRunner)
162}
163
164pub fn check_dependencies_with_runner(runner: &impl CommandRunner) -> Vec<String> {
165 let mut missing = Vec::new();
166 if !check_ytdlp_with_runner(runner) {
167 missing.push("yt-dlp".into());
168 }
169 if !check_ffmpeg_with_runner(runner) {
170 missing.push("ffmpeg".into());
171 }
172 missing
173}
174
175pub fn check_dependencies() -> Vec<String> {
176 check_dependencies_with_runner(&RealCommandRunner)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::collections::HashMap;
183
184 struct MockCommandRunner {
185 responses: HashMap<String, bool>,
187 }
188
189 impl MockCommandRunner {
190 fn new() -> Self {
191 Self {
192 responses: HashMap::new(),
193 }
194 }
195
196 fn set_response(mut self, program: &str, success: bool) -> Self {
197 self.responses.insert(program.to_string(), success);
198 self
199 }
200 }
201
202 impl CommandRunner for MockCommandRunner {
203 fn check_command_success(&self, program: &str, _args: &[&str]) -> bool {
204 *self.responses.get(program).unwrap_or(&false)
205 }
206 }
207
208 #[test]
209 fn test_check_dependencies_both_present() {
210 let runner = MockCommandRunner::new()
211 .set_response("ffmpeg", true)
212 .set_response("yt-dlp", true);
213
214 assert!(check_ffmpeg_with_runner(&runner));
215 assert!(check_ytdlp_with_runner(&runner));
216 let missing = check_dependencies_with_runner(&runner);
217 assert!(missing.is_empty());
218 }
219
220 #[test]
221 fn test_check_dependencies_ffmpeg_missing() {
222 let runner = MockCommandRunner::new()
223 .set_response("ffmpeg", false)
224 .set_response("yt-dlp", true);
225
226 assert!(!check_ffmpeg_with_runner(&runner));
227 assert!(check_ytdlp_with_runner(&runner));
228 let missing = check_dependencies_with_runner(&runner);
229 assert_eq!(missing, vec!["ffmpeg".to_string()]);
230 }
231
232 #[test]
233 fn test_check_dependencies_ytdlp_missing() {
234 let runner = MockCommandRunner::new()
235 .set_response("ffmpeg", true)
236 .set_response("yt-dlp", false);
237
238 assert!(check_ffmpeg_with_runner(&runner));
239 assert!(!check_ytdlp_with_runner(&runner));
240 let missing = check_dependencies_with_runner(&runner);
241 assert_eq!(missing, vec!["yt-dlp".to_string()]);
242 }
243
244 #[test]
245 fn test_check_dependencies_both_missing() {
246 let runner = MockCommandRunner::new()
247 .set_response("ffmpeg", false)
248 .set_response("yt-dlp", false);
249
250 assert!(!check_ffmpeg_with_runner(&runner));
251 assert!(!check_ytdlp_with_runner(&runner));
252 let missing = check_dependencies_with_runner(&runner);
253 assert_eq!(missing, vec!["yt-dlp".to_string(), "ffmpeg".to_string()]);
254 }
255}