Skip to main content

yt_dlp/extractor/
mod.rs

1//! Video extractor system for multi-site support.
2//!
3//! This module provides a trait-based architecture for handling different video sites:
4//! - `Youtube`: Highly optimized extractor for YouTube with platform-specific features
5//! - `Generic`: Universal extractor for all other yt-dlp supported sites
6//!
7//! The `Downloader` struct automatically detects and uses the appropriate extractor.
8
9use 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/// Identifies which extractor implementation is in use.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum ExtractorName {
21    /// YouTube-specific extractor with platform optimizations.
22    Youtube,
23    /// Generic extractor for all other yt-dlp supported sites.
24    /// Contains the optional site-specific extractor name reported by yt-dlp
25    /// (e.g. `"vimeo"`, `"tiktok"`).
26    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/// Core trait for video extractors.
40///
41/// This trait defines the common interface that all extractors must implement.
42/// Each extractor handles fetching video metadata and playlists from their respective platform.
43#[async_trait]
44pub trait VideoExtractor: Downcast + Send + Sync + fmt::Debug {
45    /// Fetch video metadata from a URL.
46    ///
47    /// # Arguments
48    ///
49    /// * `url` - The video URL to fetch
50    ///
51    /// # Returns
52    ///
53    /// Video metadata including formats, title, duration, etc.
54    ///
55    /// # Errors
56    ///
57    /// Returns error if the URL is unsupported, geo-blocked, or requires authentication
58    async fn fetch_video(&self, url: &str) -> Result<Video>;
59
60    /// Fetch playlist metadata from a URL.
61    ///
62    /// # Arguments
63    ///
64    /// * `url` - The playlist URL to fetch
65    ///
66    /// # Returns
67    ///
68    /// Playlist metadata including entries and metadata
69    ///
70    /// # Errors
71    ///
72    /// Returns error if the URL is unsupported or invalid
73    async fn fetch_playlist(&self, url: &str) -> Result<Playlist>;
74
75    /// Get the name of this extractor.
76    fn name(&self) -> ExtractorName;
77
78    /// Check if this extractor supports the given URL pattern.
79    ///
80    /// This is a fast, synchronous check based on URL patterns.
81    /// Use `fetch_video()` for definitive validation.
82    fn supports_url(&self, url: &str) -> bool;
83}
84
85impl_downcast!(VideoExtractor);
86
87/// Common configuration methods for all extractors.
88pub trait ExtractorConfig: VideoExtractor {
89    /// Returns a mutable reference to the internal args vector.
90    fn args_mut(&mut self) -> &mut Vec<String>;
91
92    /// Returns a mutable reference to the internal timeout.
93    fn timeout_mut(&mut self) -> &mut Duration;
94
95    /// Add custom yt-dlp argument.
96    fn with_arg(&mut self, arg: String) -> &mut Self {
97        self.args_mut().push(arg);
98        self
99    }
100
101    /// Set timeout for yt-dlp operations.
102    fn with_timeout(&mut self, timeout: Duration) -> &mut Self {
103        *self.timeout_mut() = timeout;
104        self
105    }
106
107    /// Use a Netscape cookie file for authentication.
108    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    /// Extract cookies from a browser for authentication.
114    fn with_cookies_from_browser(&mut self, browser: &str) -> &mut Self {
115        self.with_arg(format!("--cookies-from-browser={}", browser))
116    }
117
118    /// Use .netrc for authentication.
119    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/// Common logic for extractors to execute yt-dlp and parse output.
129#[async_trait]
130pub trait ExtractorBase: VideoExtractor {
131    /// Get the executable path.
132    fn executable_path(&self) -> PathBuf;
133    /// Get the request timeout.
134    fn timeout(&self) -> Duration;
135    /// Build base arguments for yt-dlp.
136    fn build_base_args(&self) -> Vec<String>;
137
138    /// Fetch and parse video metadata.
139    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    /// Fetch and parse playlist metadata.
146    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    /// Fetches video metadata and emits structured tracing on success or failure.
154    ///
155    /// Wraps `fetch_video_metadata` with a consistent log pattern. Call this from
156    /// `VideoExtractor::fetch_video` implementations to avoid repeating the
157    /// match-and-log boilerplate. The `extractor` string is emitted as a structured
158    /// field so each implementor can identify itself in logs.
159    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    /// Fetches playlist metadata and emits structured tracing on success or failure.
181    ///
182    /// Wraps `fetch_playlist_metadata` with a consistent log pattern. Call this from
183    /// `VideoExtractor::fetch_playlist` implementations to avoid repeating the
184    /// match-and-log boilerplate.
185    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
207/// Implements [`ExtractorConfig`] for a struct with `args: Vec<String>` and `timeout: Duration` fields.
208///
209/// Both fields must be named exactly `args` and `timeout`.
210macro_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
233/// Internal generic helper: execute yt-dlp and parse its JSON output as `T`.
234///
235/// Handles the common pattern of creating an `Executor`, writing output to a
236/// temporary file, and deserialising it with `serde_json` inside `spawn_blocking`.
237async 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
279/// Helper to execute the extractor command and parse the output as a Video.
280///
281/// # Errors
282///
283/// Returns an error if execution fails, JSON parsing fails, or the operation times out
284pub 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
303/// Helper to execute the extractor command and parse the output as a Playlist.
304///
305/// # Errors
306///
307/// Returns an error if execution fails, JSON parsing fails, or the operation times out
308pub 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}