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}