Skip to main content

mangofetch_core/core/
media_processor.rs

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        // maps "program" -> success status
186        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}