Skip to main content

yt_dlp/extractor/
youtube.rs

1//! YouTube extractor with platform-specific optimizations.
2//!
3//! This extractor provides highly optimized YouTube downloading with:
4//! - Player client selection (Android, iOS, Web, TV Embedded)
5//! - Format presets for common use cases
6//! - YouTube-specific shortcuts (channel, user, search)
7//! - Performance optimizations
8
9use std::path::PathBuf;
10use std::time::Duration;
11
12use async_trait::async_trait;
13
14use crate::error::Result;
15use crate::extractor::{ExtractorBase, VideoExtractor, execute_and_parse_playlist, execute_and_parse_video};
16use crate::model::Video;
17use crate::model::playlist::Playlist;
18
19/// YouTube player client types.
20///
21/// Different player clients have different capabilities and performance characteristics.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PlayerClient {
24    /// Android client (bypasses some throttling, works on restricted videos)
25    Android,
26    /// iOS client (good quality, reliable)
27    IOS,
28    /// Web client (all formats available, well-tested)
29    Web,
30    /// TV Embedded client (bypasses age restrictions)
31    TvEmbedded,
32}
33
34impl PlayerClient {
35    fn as_arg(&self) -> &str {
36        match self {
37            PlayerClient::Android => "android",
38            PlayerClient::IOS => "ios",
39            PlayerClient::Web => "web",
40            PlayerClient::TvEmbedded => "tv_embedded",
41        }
42    }
43}
44
45/// Format preset for YouTube downloads.
46///
47/// These presets provide common format selection patterns optimized for different use cases.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum FormatPreset {
50    /// Best available quality (highest resolution + best audio)
51    Best,
52    /// Premium quality (1080p+ with high bitrate audio)
53    Premium,
54    /// High quality (1080p with good audio)
55    High,
56    /// Medium quality (720p with standard audio)
57    Medium,
58    /// Low quality (480p or lower, smaller file size)
59    Low,
60    /// Audio only (best audio quality)
61    AudioOnly,
62    /// Modern codecs (VP9/AV1 + Opus for smaller files)
63    ModernCodecs,
64    /// Legacy compatibility (H.264 + AAC for older devices)
65    LegacyCompatible,
66    /// Custom format selector string
67    Custom(String),
68}
69
70impl FormatPreset {
71    fn to_format_selector(&self) -> String {
72        match self {
73            Self::Best => "bestvideo+bestaudio/best".to_string(),
74            Self::Premium => "bestvideo[height>=1080]+bestaudio[abr>=192]/best".to_string(),
75            Self::High => "bestvideo[height>=1080]+bestaudio/best".to_string(),
76            Self::Medium => "bestvideo[height<=720]+bestaudio/best".to_string(),
77            Self::Low => "bestvideo[height<=480]+bestaudio/best".to_string(),
78            Self::AudioOnly => "bestaudio/best".to_string(),
79            Self::ModernCodecs => "bestvideo[vcodec^=vp9]+bestaudio[acodec=opus]/best".to_string(),
80            Self::LegacyCompatible => "best[ext=mp4]/best".to_string(),
81            Self::Custom(selector) => selector.clone(),
82        }
83    }
84}
85
86/// YouTube extractor with optimizations.
87///
88/// This struct provides access to YouTube-specific features and optimizations
89/// that go beyond generic video downloading.
90#[derive(Debug, Clone)]
91pub struct Youtube {
92    executable_path: PathBuf,
93    player_client: Option<PlayerClient>,
94    skip_dash: bool,
95    format_preset: Option<FormatPreset>,
96    args: Vec<String>,
97    timeout: Duration,
98}
99
100crate::extractor::impl_extractor_config!(Youtube);
101
102impl Youtube {
103    /// Create a new YouTube extractor.
104    ///
105    /// # Arguments
106    ///
107    /// * `executable_path` - Path to the yt-dlp executable
108    ///
109    /// # Returns
110    ///
111    /// A new Youtube extractor instance
112    pub fn new(executable_path: PathBuf) -> Self {
113        tracing::debug!(
114            executable = ?executable_path,
115            "⚙️ Creating new Youtube extractor"
116        );
117
118        Self {
119            executable_path,
120            player_client: None,
121            skip_dash: false,
122            format_preset: None,
123            args: Vec::new(),
124            timeout: crate::client::DEFAULT_TIMEOUT,
125        }
126    }
127
128    /// Set YouTube player client for optimal performance.
129    ///
130    /// # Arguments
131    ///
132    /// * `client` - The player client to use
133    ///
134    /// # Returns
135    ///
136    /// Self for method chaining
137    ///
138    /// # Examples
139    /// ```rust,no_run
140    /// # use yt_dlp::extractor::Youtube;
141    /// # use yt_dlp::extractor::youtube::PlayerClient;
142    /// # use std::path::PathBuf;
143    /// let mut extractor = Youtube::new(PathBuf::from("yt-dlp"));
144    /// extractor.with_player_client(PlayerClient::Android);
145    /// ```
146    pub fn with_player_client(&mut self, client: PlayerClient) -> &mut Self {
147        self.player_client = Some(client);
148        self
149    }
150
151    /// Skip DASH manifest for faster extraction.
152    ///
153    /// This speeds up video information fetching but may miss some formats.
154    ///
155    /// # Arguments
156    ///
157    /// * `skip` - Whether to skip DASH manifest parsing
158    ///
159    /// # Returns
160    ///
161    /// Self for method chaining
162    pub fn skip_dash_manifest(&mut self, skip: bool) -> &mut Self {
163        self.skip_dash = skip;
164        self
165    }
166
167    /// Set format preset for video quality.
168    ///
169    /// # Arguments
170    ///
171    /// * `preset` - The format preset to use
172    ///
173    /// # Returns
174    ///
175    /// Self for method chaining
176    pub fn with_format_preset(&mut self, preset: FormatPreset) -> &mut Self {
177        self.format_preset = Some(preset);
178        self
179    }
180
181    // ========== YouTube-Specific Methods ==========
182
183    /// Fetch channel by ID (fast, direct API).
184    ///
185    /// # Arguments
186    ///
187    /// * `channel_id` - The YouTube channel ID
188    ///
189    /// # Returns
190    ///
191    /// Playlist containing all channel videos
192    ///
193    /// # Errors
194    ///
195    /// Returns error if channel is not found or inaccessible
196    ///
197    /// # Examples
198    /// ```rust,no_run
199    /// # use yt_dlp::extractor::Youtube;
200    /// # use std::path::PathBuf;
201    /// # #[tokio::main]
202    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
203    /// let extractor = Youtube::new(PathBuf::from("yt-dlp"));
204    /// let channel = extractor.fetch_channel("Underscore_").await?;
205    /// # Ok(())
206    /// # }
207    /// ```
208    pub async fn fetch_channel(&self, channel_id: &str) -> Result<Playlist> {
209        tracing::debug!(channel_id = channel_id, "📡 Fetching YouTube channel by ID");
210
211        let url = format!("https://www.youtube.com/channel/{}", channel_id);
212        self.fetch_playlist(&url).await
213    }
214
215    /// Fetch channel by handle (@username).
216    ///
217    /// # Arguments
218    ///
219    /// * `handle` - The YouTube channel handle (without @)
220    ///
221    /// # Returns
222    ///
223    /// Playlist containing all channel videos
224    ///
225    /// # Errors
226    ///
227    /// Returns error if channel is not found or inaccessible
228    pub async fn fetch_channel_by_handle(&self, handle: &str) -> Result<Playlist> {
229        tracing::debug!(handle = handle, "📡 Fetching YouTube channel by handle");
230
231        let url = format!("https://www.youtube.com/@{}", handle);
232        self.fetch_playlist(&url).await
233    }
234
235    /// Fetch user's uploads (legacy URL format).
236    ///
237    /// # Arguments
238    ///
239    /// * `username` - The YouTube username
240    ///
241    /// # Returns
242    ///
243    /// Playlist containing all user videos
244    ///
245    /// # Errors
246    ///
247    /// Returns error if user is not found or inaccessible
248    pub async fn fetch_user(&self, username: &str) -> Result<Playlist> {
249        tracing::debug!(username = username, "📡 Fetching YouTube user uploads");
250
251        let url = format!("https://www.youtube.com/user/{}", username);
252        self.fetch_playlist(&url).await
253    }
254
255    /// Fetch playlist with pagination control.
256    ///
257    /// # Arguments
258    ///
259    /// * `playlist_id` - The YouTube playlist ID
260    /// * `start` - Starting video index (1-based)
261    /// * `count` - Number of videos to fetch
262    ///
263    /// # Returns
264    ///
265    /// Playlist containing specified range of videos
266    ///
267    /// # Errors
268    ///
269    /// Returns error if playlist is not found or inaccessible
270    pub async fn fetch_playlist_paginated(&self, playlist_id: &str, start: usize, count: usize) -> Result<Playlist> {
271        let end = start.saturating_add(count).saturating_sub(1);
272        tracing::debug!(
273            playlist_id = playlist_id,
274            start = start,
275            count = count,
276            end = end,
277            "📡 Fetching paginated YouTube playlist"
278        );
279
280        let mut args = self.build_base_args();
281        args.push("--flat-playlist".to_string());
282        args.push(format!("--playlist-start={}", start));
283        args.push(format!("--playlist-end={}", end));
284
285        let url = format!("https://www.youtube.com/playlist?list={}", playlist_id);
286        args.push(url);
287
288        execute_and_parse_playlist(self.executable_path(), &args, self.timeout()).await
289    }
290
291    /// Search YouTube videos.
292    ///
293    /// # Arguments
294    ///
295    /// * `query` - The search query
296    /// * `max_results` - Maximum number of results to return
297    ///
298    /// # Returns
299    ///
300    /// Playlist containing search results
301    ///
302    /// # Errors
303    ///
304    /// Returns error if search fails
305    ///
306    /// # Examples
307    /// ```rust,no_run
308    /// # use yt_dlp::extractor::Youtube;
309    /// # use std::path::PathBuf;
310    /// # #[tokio::main]
311    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
312    /// let extractor = Youtube::new(PathBuf::from("yt-dlp"));
313    /// let results = extractor.search("rust programming", 10).await?;
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub async fn search(&self, query: &str, max_results: usize) -> Result<Playlist> {
318        tracing::debug!(query = query, max_results = max_results, "📡 Searching YouTube videos");
319
320        let url = format!("ytsearch{}:{}", max_results, query);
321        self.fetch_playlist(&url).await
322    }
323
324    /// Search and return first result.
325    ///
326    /// # Arguments
327    ///
328    /// * `query` - The search query
329    ///
330    /// # Returns
331    ///
332    /// First video matching the search
333    ///
334    /// # Errors
335    ///
336    /// Returns error if no results found
337    pub async fn search_first(&self, query: &str) -> Result<Video> {
338        tracing::debug!(query = query, "📡 Searching for first YouTube video result");
339
340        let url = format!("ytsearch1:{}", query);
341        let mut args = self.build_base_args();
342        args.push(url);
343
344        execute_and_parse_video(self.executable_path(), &args, self.timeout()).await
345    }
346
347    /// Check if URL is supported by YouTube extractor.
348    pub fn supports_url(url: &str) -> bool {
349        let url_lower = url.to_lowercase();
350
351        // Check search/playlist prefixes first
352        let has_valid_prefix = ["ytsearch", "ytplaylist"]
353            .iter()
354            .any(|prefix| url_lower.starts_with(prefix));
355
356        if has_valid_prefix {
357            return true;
358        }
359
360        // Extract host from URL to avoid substring false positives (e.g. "notyoutube.com")
361        let host = url_lower
362            .split("://")
363            .nth(1)
364            .unwrap_or(&url_lower)
365            .split('/')
366            .next()
367            .unwrap_or("")
368            .split(':')
369            .next()
370            .unwrap_or("");
371
372        ["youtube.com", "youtu.be", "youtube-nocookie.com"]
373            .iter()
374            .any(|domain| host == *domain || host.ends_with(&format!(".{}", domain)))
375    }
376}
377
378#[async_trait]
379impl ExtractorBase for Youtube {
380    fn executable_path(&self) -> PathBuf {
381        self.executable_path.clone()
382    }
383
384    fn timeout(&self) -> Duration {
385        self.timeout
386    }
387
388    fn build_base_args(&self) -> Vec<String> {
389        let mut args = vec!["--no-progress".to_string(), "--dump-single-json".to_string()];
390
391        // Build extractor args (must be merged into a single --extractor-args flag)
392        let mut extractor_parts = Vec::new();
393        if let Some(client) = self.player_client {
394            extractor_parts.push(format!("player_client={}", client.as_arg()));
395        }
396        if self.skip_dash {
397            extractor_parts.push("skip=dash".to_string());
398        }
399        if !extractor_parts.is_empty() {
400            args.push("--extractor-args".to_string());
401            args.push(format!("youtube:{}", extractor_parts.join(";")));
402        }
403
404        // Format preset
405        if let Some(preset) = &self.format_preset {
406            args.push("-f".to_string());
407            args.push(preset.to_format_selector());
408        }
409
410        // Custom args
411        args.extend(self.args.clone());
412
413        args
414    }
415}
416
417#[async_trait]
418impl VideoExtractor for Youtube {
419    async fn fetch_video(&self, url: &str) -> Result<Video> {
420        tracing::debug!(
421            url = url,
422            player_client = ?self.player_client,
423            skip_dash = self.skip_dash,
424            format_preset = ?self.format_preset,
425            "📡 Fetching video with Youtube extractor"
426        );
427        self.log_and_fetch_video(url, "Youtube").await
428    }
429
430    async fn fetch_playlist(&self, url: &str) -> Result<Playlist> {
431        tracing::debug!(
432            url = url,
433            player_client = ?self.player_client,
434            "📡 Fetching playlist with Youtube extractor"
435        );
436        self.log_and_fetch_playlist(url, "Youtube").await
437    }
438
439    fn name(&self) -> crate::extractor::ExtractorName {
440        crate::extractor::ExtractorName::Youtube
441    }
442
443    fn supports_url(&self, url: &str) -> bool {
444        Self::supports_url(url)
445    }
446}