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}