yt_transcript_rs/
api.rs

1use reqwest::Client;
2use std::path::Path;
3use std::sync::Arc;
4
5use crate::cookie_jar_loader::CookieJarLoader;
6#[cfg(not(feature = "ci"))]
7use crate::errors::{CookieError, CouldNotRetrieveTranscript};
8#[cfg(feature = "ci")]
9use crate::errors::{CookieError, CouldNotRetrieveTranscript, CouldNotRetrieveTranscriptReason};
10use crate::models::{MicroformatData, StreamingData, VideoDetails, VideoInfos};
11use crate::proxies::ProxyConfig;
12#[cfg(not(feature = "ci"))]
13use crate::video_data_fetcher::VideoDataFetcher;
14use crate::{FetchedTranscript, TranscriptList};
15
16/// # YouTubeTranscriptApi
17///
18/// The main interface for retrieving YouTube video transcripts and metadata.
19///
20/// This API provides methods to:
21/// - Fetch transcripts from YouTube videos in various languages
22/// - List all available transcript languages for a video
23/// - Retrieve detailed video metadata
24///
25/// The API supports advanced features like:
26/// - Custom HTTP clients and proxies for handling geo-restrictions
27/// - Cookie management for accessing restricted content
28/// - Preserving text formatting in transcripts
29///
30/// ## Simple Usage Example
31///
32/// ```rust,no_run
33/// use yt_transcript_rs::api::YouTubeTranscriptApi;
34///
35/// #[tokio::main]
36/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
37///     // Create a new API instance with default settings
38///     let api = YouTubeTranscriptApi::new(None, None, None)?;
39///     
40///     // Fetch an English transcript
41///     let transcript = api.fetch_transcript(
42///         "dQw4w9WgXcQ",      // Video ID
43///         &["en"],            // Preferred languages
44///         false               // Don't preserve formatting
45///     ).await?;
46///     
47///     // Print each snippet of the transcript
48///     for snippet in transcript.parts() {
49///         println!("[{:.1}s]: {}", snippet.start, snippet.text);
50///     }
51///     
52///     Ok(())
53/// }
54/// ```
55#[derive(Clone)]
56pub struct YouTubeTranscriptApi {
57    /// The internal data fetcher used to retrieve information from YouTube
58    #[cfg(not(feature = "ci"))]
59    fetcher: Arc<VideoDataFetcher>,
60    #[cfg(feature = "ci")]
61    client: Client,
62}
63
64impl YouTubeTranscriptApi {
65    /// Creates a new YouTube Transcript API instance.
66    ///
67    /// This method initializes an API instance with optional customizations for
68    /// cookies, proxies, and HTTP client settings.
69    ///
70    /// # Parameters
71    ///
72    /// * `cookie_path` - Optional path to a Netscape-format cookie file for authenticated requests
73    /// * `proxy_config` - Optional proxy configuration for routing requests through a proxy service
74    /// * `http_client` - Optional pre-configured HTTP client to use instead of the default one
75    ///
76    /// # Returns
77    ///
78    /// * `Result<Self, CookieError>` - A new API instance or a cookie-related error
79    ///
80    /// # Errors
81    ///
82    /// This function will return an error if:
83    /// - The cookie file exists but cannot be read or parsed
84    /// - The cookie file is not in the expected Netscape format
85    ///
86    /// # Examples
87    ///
88    /// ## Basic usage with default settings
89    ///
90    /// ```rust,no_run
91    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
92    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
93    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
94    /// # Ok(())
95    /// # }
96    /// ```
97    ///
98    /// ## Using a cookie file for authenticated access
99    ///
100    /// ```rust,no_run
101    /// # use std::path::Path;
102    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
103    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
104    /// let cookie_path = Path::new("path/to/cookies.txt");
105    /// let api = YouTubeTranscriptApi::new(Some(&cookie_path), None, None)?;
106    /// # Ok(())
107    /// # }
108    /// ```
109    ///
110    /// ## Using a proxy to bypass geographical restrictions
111    ///
112    /// ```rust,no_run
113    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
114    /// # use yt_transcript_rs::proxies::GenericProxyConfig;
115    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
116    /// // Create a proxy configuration
117    /// let proxy = GenericProxyConfig::new(
118    ///     Some("http://proxy.example.com:8080".to_string()),
119    ///     None
120    /// )?;
121    ///
122    /// let api = YouTubeTranscriptApi::new(
123    ///     None,
124    ///     Some(Box::new(proxy)),
125    ///     None
126    /// )?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub fn new(
131        cookie_path: Option<&Path>,
132        proxy_config: Option<Box<dyn ProxyConfig + Send + Sync>>,
133        http_client: Option<Client>,
134    ) -> Result<Self, CookieError> {
135        let client = match http_client {
136            Some(client) => client,
137            None => {
138                let mut builder = Client::builder()
139                    .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
140                    .default_headers({
141                        let mut headers = reqwest::header::HeaderMap::new();
142                        headers.insert(
143                            reqwest::header::ACCEPT_LANGUAGE,
144                            reqwest::header::HeaderValue::from_static("en-US"),
145                        );
146                        headers
147                    });
148
149                // Add cookie jar if needed
150                if let Some(cookie_path) = cookie_path {
151                    let cookie_jar = CookieJarLoader::load_cookie_jar(cookie_path)?;
152                    let cookie_jar = Arc::new(cookie_jar);
153                    builder = builder.cookie_store(true).cookie_provider(cookie_jar);
154                }
155
156                // Add proxy configuration if needed
157                if let Some(proxy_config_ref) = &proxy_config {
158                    // Convert the proxy configuration to a map first to avoid borrowing issues
159                    let proxy_map = proxy_config_ref.to_requests_dict();
160
161                    let proxies = reqwest::Proxy::custom(move |url| {
162                        if url.scheme() == "http" {
163                            if let Some(http_proxy) = proxy_map.get("http") {
164                                return Some(http_proxy.clone());
165                            }
166                        } else if url.scheme() == "https" {
167                            if let Some(https_proxy) = proxy_map.get("https") {
168                                return Some(https_proxy.clone());
169                            }
170                        }
171
172                        None
173                    });
174
175                    builder = builder.proxy(proxies);
176
177                    // Disable keep-alive if needed
178                    if proxy_config_ref.prevent_keeping_connections_alive() {
179                        builder = builder.connection_verbose(true).tcp_keepalive(None);
180
181                        let mut headers = reqwest::header::HeaderMap::new();
182                        headers.insert(
183                            reqwest::header::CONNECTION,
184                            reqwest::header::HeaderValue::from_static("close"),
185                        );
186                        builder = builder.default_headers(headers);
187                    }
188                }
189
190                builder.build().unwrap()
191            }
192        };
193
194        #[cfg(not(feature = "ci"))]
195        let fetcher = Arc::new(VideoDataFetcher::new(client.clone()));
196
197        Ok(Self {
198            #[cfg(not(feature = "ci"))]
199            fetcher,
200            #[cfg(feature = "ci")]
201            client,
202        })
203    }
204
205    /// Fetches a transcript for a YouTube video in the specified languages.
206    ///
207    /// This method attempts to retrieve a transcript in the first available language
208    /// from the provided list of language preferences. If none of the specified languages
209    /// are available, an error is returned.
210    ///
211    /// # Parameters
212    ///
213    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ" from https://www.youtube.com/watch?v=dQw4w9WgXcQ)
214    /// * `languages` - A list of language codes in order of preference (e.g., ["en", "es", "fr"])
215    /// * `preserve_formatting` - Whether to preserve HTML formatting in the transcript text
216    ///
217    /// # Returns
218    ///
219    /// * `Result<FetchedTranscript, CouldNotRetrieveTranscript>` - The transcript or an error
220    ///
221    /// # Errors
222    ///
223    /// This method will return an error if:
224    /// - The video does not exist or is private
225    /// - The video has no transcripts available
226    /// - None of the requested languages are available
227    /// - Network issues prevent fetching the transcript
228    ///
229    /// # Examples
230    ///
231    /// ## Basic usage - get English transcript
232    ///
233    /// ```rust,no_run
234    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
235    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
236    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
237    ///
238    /// // Fetch English transcript
239    /// let transcript = api.fetch_transcript(
240    ///     "dQw4w9WgXcQ",  // Video ID
241    ///     &["en"],        // Try English
242    ///     false           // Don't preserve formatting
243    /// ).await?;
244    ///
245    /// println!("Full transcript text: {}", transcript.text());
246    /// # Ok(())
247    /// # }
248    /// ```
249    ///
250    /// ## Multiple language preferences with formatting preserved
251    ///
252    /// ```rust,no_run
253    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
254    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
255    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
256    ///
257    /// // Try English first, then Spanish, then auto-generated English
258    /// let transcript = api.fetch_transcript(
259    ///     "dQw4w9WgXcQ",
260    ///     &["en", "es", "en-US"],
261    ///     true  // Preserve formatting like <b>bold</b> text
262    /// ).await?;
263    ///
264    /// // Print each segment with timing information
265    /// for snippet in transcript.parts() {
266    ///     println!("[{:.1}s-{:.1}s]: {}",
267    ///         snippet.start,
268    ///         snippet.start + snippet.duration,
269    ///         snippet.text);
270    /// }
271    /// # Ok(())
272    /// # }
273    /// ```
274    #[cfg(feature = "ci")]
275    pub async fn fetch_transcript(
276        &self,
277        video_id: &str,
278        languages: &[&str],
279        _preserve_formatting: bool,
280    ) -> Result<FetchedTranscript, CouldNotRetrieveTranscript> {
281        if video_id == crate::tests::test_utils::NON_EXISTENT_VIDEO_ID {
282            return Err(CouldNotRetrieveTranscript {
283                video_id: video_id.to_string(),
284                reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
285            });
286        }
287
288        let transcript =
289            crate::tests::mocks::create_mock_fetched_transcript(video_id, languages[0]);
290        Ok(transcript)
291    }
292
293    #[cfg(not(feature = "ci"))]
294    pub async fn fetch_transcript(
295        &self,
296        video_id: &str,
297        languages: &[&str],
298        preserve_formatting: bool,
299    ) -> Result<FetchedTranscript, CouldNotRetrieveTranscript> {
300        // First list all available transcripts
301        let transcript_list = self.list_transcripts(video_id).await?;
302
303        // Then find the best matching transcript based on language preferences
304        let transcript = transcript_list.find_transcript(languages)?;
305
306        // Use the client from the fetcher
307        let client = &self.fetcher.client;
308
309        // Finally fetch the actual transcript content
310        transcript.fetch(client, preserve_formatting).await
311    }
312
313    /// Lists all available transcripts for a YouTube video.
314    ///
315    /// This method retrieves a list of all available transcripts/captions for a video,
316    /// categorized by:
317    /// - Language
318    /// - Whether they were manually created or automatically generated
319    /// - Whether they support translation to other languages
320    ///
321    /// # Parameters
322    ///
323    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ")
324    ///
325    /// # Returns
326    ///
327    /// * `Result<TranscriptList, CouldNotRetrieveTranscript>` - A list of available transcripts or an error
328    ///
329    /// # Errors
330    ///
331    /// This method will return an error if:
332    /// - The video ID is invalid
333    /// - The video doesn't exist or is private
334    /// - YouTube is blocking your requests
335    /// - Network errors occur
336    /// - No transcripts are available for the video
337    ///
338    /// # Example
339    ///
340    /// ```rust,no_run
341    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
342    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
343    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
344    /// let video_id = "dQw4w9WgXcQ";
345    ///
346    /// let transcript_list = api.list_transcripts(video_id).await?;
347    ///
348    /// // Print available transcripts
349    /// println!("{}", transcript_list);
350    ///
351    /// // Count manually created vs. generated transcripts
352    /// println!(
353    ///     "{} manual, {} auto-generated transcripts available",
354    ///     transcript_list.manually_created_transcripts.len(),
355    ///     transcript_list.generated_transcripts.len()
356    /// );
357    /// # Ok(())
358    /// # }
359    /// ```
360    #[cfg(feature = "ci")]
361    pub async fn list_transcripts(
362        &self,
363        video_id: &str,
364    ) -> Result<TranscriptList, CouldNotRetrieveTranscript> {
365        // For non-existent video ID, return an error
366        if video_id == crate::tests::test_utils::NON_EXISTENT_VIDEO_ID {
367            return Err(CouldNotRetrieveTranscript {
368                video_id: video_id.to_string(),
369                reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
370            });
371        }
372
373        // Return mock transcript list
374        Ok(crate::tests::mocks::create_mock_transcript_list(
375            self.client.clone(),
376        ))
377    }
378
379    #[cfg(not(feature = "ci"))]
380    pub async fn list_transcripts(
381        &self,
382        video_id: &str,
383    ) -> Result<TranscriptList, CouldNotRetrieveTranscript> {
384        self.fetcher.fetch_transcript_list(video_id).await
385    }
386
387    /// Fetches detailed metadata about a YouTube video.
388    ///
389    /// This method retrieves comprehensive information about a video, including its
390    /// title, author, view count, description, thumbnails, and other metadata.
391    ///
392    /// # Parameters
393    ///
394    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ")
395    ///
396    /// # Returns
397    ///
398    /// * `Result<VideoDetails, CouldNotRetrieveTranscript>` - Video details or an error
399    ///
400    /// # Errors
401    ///
402    /// This method will return an error if:
403    /// - The video does not exist or is private
404    /// - Network issues prevent fetching the video details
405    /// - The YouTube page structure has changed and details cannot be extracted
406    ///
407    /// # Examples
408    ///
409    /// ```rust,no_run
410    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
411    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
412    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
413    ///
414    /// // Fetch details about a video
415    /// let details = api.fetch_video_details("dQw4w9WgXcQ").await?;
416    ///
417    /// // Print basic information
418    /// println!("Title: {}", details.title);
419    /// println!("Channel: {}", details.author);
420    /// println!("Views: {}", details.view_count);
421    /// println!("Duration: {} seconds", details.length_seconds);
422    ///
423    /// // Print keywords if available
424    /// if let Some(keywords) = &details.keywords {
425    ///     println!("Keywords: {}", keywords.join(", "));
426    /// }
427    ///
428    /// // Get the highest quality thumbnail
429    /// if let Some(best_thumb) = details.thumbnails.iter()
430    ///     .max_by_key(|t| t.width * t.height) {
431    ///     println!("Best thumbnail: {} ({}x{})",
432    ///         best_thumb.url, best_thumb.width, best_thumb.height);
433    /// }
434    /// # Ok(())
435    /// # }
436    /// ```
437    #[cfg(feature = "ci")]
438    pub async fn fetch_video_details(
439        &self,
440        video_id: &str,
441    ) -> Result<VideoDetails, CouldNotRetrieveTranscript> {
442        // For non-existent video ID, return an error
443        if video_id == crate::tests::test_utils::NON_EXISTENT_VIDEO_ID {
444            return Err(CouldNotRetrieveTranscript {
445                video_id: video_id.to_string(),
446                reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
447            });
448        }
449
450        // Return mock data
451        Ok(crate::tests::mocks::create_mock_video_details())
452    }
453
454    #[cfg(not(feature = "ci"))]
455    pub async fn fetch_video_details(
456        &self,
457        video_id: &str,
458    ) -> Result<VideoDetails, CouldNotRetrieveTranscript> {
459        self.fetcher.fetch_video_details(video_id).await
460    }
461
462    /// Fetches microformat data for a YouTube video.
463    ///
464    /// This method retrieves additional metadata about a video that's not included
465    /// in the main video details, such as available countries, category, and embed information.
466    ///
467    /// # Parameters
468    ///
469    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ")
470    ///
471    /// # Returns
472    ///
473    /// * `Result<MicroformatData, CouldNotRetrieveTranscript>` - Microformat data or an error
474    ///
475    /// # Errors
476    ///
477    /// This method will return an error if:
478    /// - The video does not exist or is private
479    /// - Network issues prevent fetching the data
480    /// - The YouTube page structure has changed and data cannot be extracted
481    ///
482    /// # Examples
483    ///
484    /// ```rust,no_run
485    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
486    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
487    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
488    ///
489    /// // Fetch microformat data about a video
490    /// let microformat = api.fetch_microformat("dQw4w9WgXcQ").await?;
491    ///
492    /// // Check if the video is unlisted
493    /// if let Some(is_unlisted) = microformat.is_unlisted {
494    ///     println!("Video is unlisted: {}", is_unlisted);
495    /// }
496    ///
497    /// // Get video category
498    /// if let Some(category) = microformat.category {
499    ///     println!("Video category: {}", category);
500    /// }
501    ///
502    /// // Check availability by country
503    /// if let Some(countries) = microformat.available_countries {
504    ///     println!("Video available in {} countries", countries.len());
505    ///     if countries.contains(&"US".to_string()) {
506    ///         println!("Video is available in the United States");
507    ///     }
508    /// }
509    /// # Ok(())
510    /// # }
511    /// ```
512    #[cfg(feature = "ci")]
513    pub async fn fetch_microformat(
514        &self,
515        video_id: &str,
516    ) -> Result<MicroformatData, CouldNotRetrieveTranscript> {
517        // For non-existent video ID, return an error
518        if video_id == crate::tests::test_utils::NON_EXISTENT_VIDEO_ID {
519            return Err(CouldNotRetrieveTranscript {
520                video_id: video_id.to_string(),
521                reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
522            });
523        }
524
525        // Return mock data
526        Ok(crate::tests::mocks::create_mock_microformat_data())
527    }
528
529    #[cfg(not(feature = "ci"))]
530    pub async fn fetch_microformat(
531        &self,
532        video_id: &str,
533    ) -> Result<MicroformatData, CouldNotRetrieveTranscript> {
534        self.fetcher.fetch_microformat(video_id).await
535    }
536
537    /// Fetches streaming data for a YouTube video.
538    ///
539    /// This method retrieves information about available video and audio formats, including:
540    /// - URLs for different quality versions of the video
541    /// - Resolution, bitrate, and codec information
542    /// - Both combined formats (with audio and video) and separate adaptive formats
543    /// - Information about format expiration
544    ///
545    /// # Parameters
546    ///
547    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ")
548    ///
549    /// # Returns
550    ///
551    /// * `Result<StreamingData, CouldNotRetrieveTranscript>` - Streaming data or an error
552    ///
553    /// # Errors
554    ///
555    /// This method will return an error if:
556    /// - The video does not exist or is private
557    /// - The video has geo-restrictions that prevent access
558    /// - Network issues prevent fetching the data
559    ///
560    /// # Examples
561    ///
562    /// ```rust,no_run
563    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
564    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
565    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
566    ///
567    /// // Fetch streaming data about a video
568    /// let streaming = api.fetch_streaming_data("dQw4w9WgXcQ").await?;
569    ///
570    /// // Print information about formats
571    /// println!("Combined formats: {}", streaming.formats.len());
572    /// println!("Adaptive formats: {}", streaming.adaptive_formats.len());
573    ///
574    /// // Find the highest resolution video format
575    /// if let Some(best_video) = streaming.adaptive_formats.iter()
576    ///     .filter(|f| f.width.is_some() && f.height.is_some())
577    ///     .max_by_key(|f| f.height.unwrap_or(0)) {
578    ///     println!("Best video quality: {}p", best_video.height.unwrap());
579    ///     println!("Codec: {}", best_video.mime_type);
580    /// }
581    ///
582    /// // Find the best audio format
583    /// if let Some(best_audio) = streaming.adaptive_formats.iter()
584    ///     .filter(|f| f.audio_quality.is_some())
585    ///     .max_by_key(|f| f.bitrate) {
586    ///     println!("Best audio quality: {}", best_audio.audio_quality.as_ref().unwrap());
587    ///     println!("Bitrate: {} bps", best_audio.bitrate);
588    /// }
589    /// # Ok(())
590    /// # }
591    /// ```
592    #[cfg(feature = "ci")]
593    pub async fn fetch_streaming_data(
594        &self,
595        video_id: &str,
596    ) -> Result<StreamingData, CouldNotRetrieveTranscript> {
597        // For non-existent video ID, return an error
598        if video_id == crate::tests::test_utils::NON_EXISTENT_VIDEO_ID {
599            return Err(CouldNotRetrieveTranscript {
600                video_id: video_id.to_string(),
601                reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
602            });
603        }
604
605        // Return mock data
606        Ok(crate::tests::mocks::create_mock_streaming_data())
607    }
608
609    #[cfg(not(feature = "ci"))]
610    pub async fn fetch_streaming_data(
611        &self,
612        video_id: &str,
613    ) -> Result<StreamingData, CouldNotRetrieveTranscript> {
614        self.fetcher.fetch_streaming_data(video_id).await
615    }
616
617    /// Fetches all available information about a YouTube video in a single request.
618    ///
619    /// This method retrieves comprehensive information about a video in one network call, including:
620    /// - Video details (title, author, etc.)
621    /// - Microformat data (category, available countries, etc.)
622    /// - Streaming data (available formats, qualities, etc.)
623    /// - Transcript list (available caption languages)
624    ///
625    /// This is more efficient than calling individual methods separately when multiple
626    /// types of information are needed, as it avoids multiple HTTP requests.
627    ///
628    /// # Parameters
629    ///
630    /// * `video_id` - The YouTube video ID (e.g., "dQw4w9WgXcQ")
631    ///
632    /// # Returns
633    ///
634    /// * `Result<VideoInfos, CouldNotRetrieveTranscript>` - Combined video information on success, or an error
635    ///
636    /// # Errors
637    ///
638    /// This method will return a `CouldNotRetrieveTranscript` error if:
639    /// - The video doesn't exist or is private
640    /// - The video has geo-restrictions that prevent access
641    /// - Network errors occur during the request
642    ///
643    /// # Examples
644    ///
645    /// ```rust,no_run
646    /// # use yt_transcript_rs::api::YouTubeTranscriptApi;
647    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
648    /// let api = YouTubeTranscriptApi::new(None, None, None)?;
649    /// let video_id = "dQw4w9WgXcQ";
650    ///
651    /// // Fetch all information in a single request
652    /// let infos = api.fetch_video_infos(video_id).await?;
653    ///
654    /// // Access combined information
655    /// println!("Title: {}", infos.video_details.title);
656    /// println!("Author: {}", infos.video_details.author);
657    ///
658    /// if let Some(category) = &infos.microformat.category {
659    ///     println!("Category: {}", category);
660    /// }
661    ///
662    /// println!("Available formats: {}", infos.streaming_data.formats.len());
663    /// println!("Available transcripts: {}", infos.transcript_list.transcripts().count());
664    /// # Ok(())
665    /// # }
666    /// ```
667    #[cfg(not(feature = "ci"))]
668    pub async fn fetch_video_infos(
669        &self,
670        video_id: &str,
671    ) -> Result<VideoInfos, CouldNotRetrieveTranscript> {
672        self.fetcher.fetch_video_infos(video_id).await
673    }
674
675    /// Fetches all available information about a YouTube video in a single request.
676    ///
677    /// This is a CI-mode placeholder that always returns an error.
678    #[cfg(feature = "ci")]
679    pub async fn fetch_video_infos(
680        &self,
681        video_id: &str,
682    ) -> Result<VideoInfos, CouldNotRetrieveTranscript> {
683        Err(CouldNotRetrieveTranscript {
684            video_id: video_id.to_string(),
685            reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
686        })
687    }
688}