1use std::fmt;
10
11use async_trait::async_trait;
12use downcast_rs::{Downcast, impl_downcast};
13
14use crate::error::Result;
15use crate::model::Video;
16use crate::model::playlist::Playlist;
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum ExtractorName {
21 Youtube,
23 Generic(Option<String>),
27}
28
29impl fmt::Display for ExtractorName {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Youtube => f.write_str("Youtube"),
33 Self::Generic(Some(name)) => write!(f, "Generic(name={})", name),
34 Self::Generic(None) => f.write_str("Generic"),
35 }
36 }
37}
38
39#[async_trait]
44pub trait VideoExtractor: Downcast + Send + Sync + fmt::Debug {
45 async fn fetch_video(&self, url: &str) -> Result<Video>;
59
60 async fn fetch_playlist(&self, url: &str) -> Result<Playlist>;
74
75 fn name(&self) -> ExtractorName;
77
78 fn supports_url(&self, url: &str) -> bool;
83}
84
85impl_downcast!(VideoExtractor);
86
87pub trait ExtractorConfig: VideoExtractor {
89 fn args_mut(&mut self) -> &mut Vec<String>;
91
92 fn timeout_mut(&mut self) -> &mut Duration;
94
95 fn with_arg(&mut self, arg: String) -> &mut Self {
97 self.args_mut().push(arg);
98 self
99 }
100
101 fn with_timeout(&mut self, timeout: Duration) -> &mut Self {
103 *self.timeout_mut() = timeout;
104 self
105 }
106
107 fn with_cookies(&mut self, path: impl AsRef<Path>) -> &mut Self {
109 let cookie_path = path.as_ref().display().to_string();
110 self.with_arg(format!("--cookies={}", cookie_path))
111 }
112
113 fn with_cookies_from_browser(&mut self, browser: &str) -> &mut Self {
115 self.with_arg(format!("--cookies-from-browser={}", browser))
116 }
117
118 fn with_netrc(&mut self) -> &mut Self {
120 self.with_arg("--netrc".to_string())
121 }
122}
123
124pub mod detector;
125pub mod generic;
126pub mod youtube;
127
128#[async_trait]
130pub trait ExtractorBase: VideoExtractor {
131 fn executable_path(&self) -> PathBuf;
133 fn timeout(&self) -> Duration;
135 fn build_base_args(&self) -> Vec<String>;
137
138 async fn fetch_video_metadata(&self, url: &str) -> Result<Video> {
140 let mut args = self.build_base_args();
141 args.push(url.to_string());
142 execute_and_parse_video(self.executable_path(), &args, self.timeout()).await
143 }
144
145 async fn fetch_playlist_metadata(&self, url: &str) -> Result<Playlist> {
147 let mut args = self.build_base_args();
148 args.push("--flat-playlist".to_string());
149 args.push(url.to_string());
150 execute_and_parse_playlist(self.executable_path(), &args, self.timeout()).await
151 }
152
153 async fn log_and_fetch_video(&self, url: &str, extractor: &str) -> Result<Video> {
160 let result = self.fetch_video_metadata(url).await;
161 match &result {
162 Ok(video) => tracing::debug!(
163 url = url,
164 extractor = extractor,
165 video_id = video.id,
166 title = video.title,
167 format_count = video.formats.len(),
168 "✅ Video fetched successfully"
169 ),
170 Err(e) => tracing::warn!(
171 url = url,
172 extractor = extractor,
173 error = %e,
174 "Failed to fetch video"
175 ),
176 }
177 result
178 }
179
180 async fn log_and_fetch_playlist(&self, url: &str, extractor: &str) -> Result<Playlist> {
186 let result = self.fetch_playlist_metadata(url).await;
187 match &result {
188 Ok(playlist) => tracing::debug!(
189 url = url,
190 extractor = extractor,
191 playlist_id = playlist.id,
192 title = playlist.title,
193 entry_count = playlist.entries.len(),
194 "✅ Playlist fetched successfully"
195 ),
196 Err(e) => tracing::warn!(
197 url = url,
198 extractor = extractor,
199 error = %e,
200 "Failed to fetch playlist"
201 ),
202 }
203 result
204 }
205}
206
207macro_rules! impl_extractor_config {
211 ($type:path) => {
212 impl $crate::extractor::ExtractorConfig for $type {
213 fn args_mut(&mut self) -> &mut Vec<String> {
214 &mut self.args
215 }
216
217 fn timeout_mut(&mut self) -> &mut std::time::Duration {
218 &mut self.timeout
219 }
220 }
221 };
222}
223use std::path::{Path, PathBuf};
224use std::time::Duration;
225
226pub use detector::detect_extractor_type;
227pub use generic::Generic;
228pub(crate) use impl_extractor_config;
229pub use youtube::Youtube;
230
231use crate::executor::Executor;
232
233async fn execute_and_parse<T>(
238 executable_path: PathBuf,
239 args: &[String],
240 timeout: Duration,
241 label: &'static str,
242) -> Result<T>
243where
244 T: serde::de::DeserializeOwned + Send + 'static,
245{
246 tracing::debug!(
247 executable = ?executable_path,
248 arg_count = args.len(),
249 timeout_secs = timeout.as_secs(),
250 "📡 Executing extractor for {label}"
251 );
252
253 let executor = Executor::new(executable_path.clone(), args.to_vec(), timeout);
254
255 let temp_dir = tempfile::tempdir()?;
256 let output_path = temp_dir.path().join(format!("{}_{}.json", label, uuid::Uuid::new_v4()));
257
258 tracing::debug!(
259 executable = ?executable_path,
260 output_path = ?output_path,
261 "📡 Redirecting yt-dlp output to temporary file"
262 );
263
264 let _output = executor.execute_to_file(&output_path).await?;
265
266 tracing::debug!(output_path = ?output_path, "⚙️ Opening output file for parsing");
267
268 let file = tokio::fs::File::open(&output_path).await?;
269 let file = file.into_std().await;
270
271 tracing::debug!("⚙️ Spawning blocking task for JSON parsing");
272
273 let result: T =
274 tokio::task::spawn_blocking(move || serde_json::from_reader(std::io::BufReader::new(file))).await??;
275
276 Ok(result)
277}
278
279pub async fn execute_and_parse_video(executable_path: PathBuf, args: &[String], timeout: Duration) -> Result<Video> {
285 let mut video: Video = execute_and_parse(executable_path, args, timeout, "video").await?;
286
287 tracing::debug!(
288 video_id = %video.id,
289 title = %video.title,
290 format_count = video.formats.len(),
291 "✅ Video parsed successfully"
292 );
293
294 for format in &mut video.formats {
295 format.video_id = Some(video.id.clone());
296 }
297
298 tracing::debug!(video_id = %video.id, "⚙️ Set video_id on all formats");
299
300 Ok(video)
301}
302
303pub async fn execute_and_parse_playlist(
309 executable_path: PathBuf,
310 args: &[String],
311 timeout: Duration,
312) -> Result<Playlist> {
313 let playlist: Playlist = execute_and_parse(executable_path, args, timeout, "playlist").await?;
314
315 tracing::debug!(
316 playlist_id = %playlist.id,
317 title = %playlist.title,
318 entry_count = playlist.entries.len(),
319 "✅ Playlist parsed successfully"
320 );
321
322 Ok(playlist)
323}