yt_transcript_rs/
errors.rs

1use crate::proxies::{GenericProxyConfig, ProxyConfig, WebshareProxyConfig};
2use crate::TranscriptList;
3use thiserror::Error;
4
5/// # YouTubeTranscriptApiError
6///
7/// The base error type for the library.
8///
9/// This is mainly used as a generic error type for cases that don't fall into
10/// more specific error categories.
11#[derive(Debug, Error)]
12pub enum YouTubeTranscriptApiError {
13    #[error("YouTube Transcript API error")]
14    Generic,
15}
16
17/// # CookieError
18///
19/// Errors related to cookie handling and authentication.
20///
21/// These errors occur when there are issues with loading or using cookies
22/// for authenticated requests to YouTube.
23#[derive(Debug, Error)]
24pub enum CookieError {
25    #[error("Cookie error")]
26    Generic,
27
28    /// Error when the specified cookie file path is invalid or inaccessible
29    #[error("Can't load the provided cookie file: {0}")]
30    PathInvalid(String),
31
32    /// Error when the cookies are invalid (possibly expired or malformed)
33    #[error("The cookies provided are not valid (may have expired): {0}")]
34    Invalid(String),
35}
36
37/// Type alias for cookie path invalid errors
38pub type CookiePathInvalid = CookieError;
39
40/// Type alias for invalid cookie errors
41pub type CookieInvalid = CookieError;
42
43/// # CouldNotRetrieveTranscript
44///
45/// The primary error type when transcript retrieval fails.
46///
47/// This error provides detailed information about why a transcript couldn't be retrieved,
48/// with specific reasons and helpful suggestions for resolving the issue.
49///
50/// ## Usage Example
51///
52/// ```rust,no_run
53/// # use yt_transcript_rs::YouTubeTranscriptApi;
54/// # async fn example() {
55/// let api = YouTubeTranscriptApi::new(None, None, None).unwrap();
56///
57/// match api.fetch_transcript("dQw4w9WgXcQ", &["en"], false).await {
58///     Ok(transcript) => {
59///         println!("Successfully retrieved transcript");
60///     },
61///     Err(err) => {
62///         // The error message contains detailed information about what went wrong
63///         eprintln!("Error: {}", err);
64///         
65///         // You can also check for specific error types
66///         if err.reason.is_some() {
67///             match err.reason.as_ref().unwrap() {
68///                 // Handle specific error cases
69///                 _ => {}
70///             }
71///         }
72///     }
73/// }
74/// # }
75/// ```
76#[derive(Debug, Error)]
77#[error("{}", self.build_error_message())]
78pub struct CouldNotRetrieveTranscript {
79    /// The YouTube video ID that was being accessed
80    pub video_id: String,
81
82    /// The specific reason why the transcript couldn't be retrieved
83    pub reason: Option<CouldNotRetrieveTranscriptReason>,
84}
85
86/// # CouldNotRetrieveTranscriptReason
87///
88/// Detailed reasons why a transcript couldn't be retrieved.
89///
90/// This enum provides specific information about why transcript retrieval failed,
91/// which is useful for error handling and providing helpful feedback to users.
92#[derive(Debug)]
93pub enum CouldNotRetrieveTranscriptReason {
94    /// Subtitles/transcripts are disabled for this video
95    TranscriptsDisabled,
96
97    /// No transcript was found in any of the requested languages
98    NoTranscriptFound {
99        /// The language codes that were requested but not found
100        requested_language_codes: Vec<String>,
101
102        /// Information about available transcripts that could be used instead
103        transcript_data: TranscriptList,
104    },
105
106    /// The video is no longer available (removed, private, etc.)
107    VideoUnavailable,
108
109    /// The video cannot be played for some reason
110    VideoUnplayable {
111        /// The main reason why the video is unplayable
112        reason: Option<String>,
113
114        /// Additional details about why the video is unplayable
115        sub_reasons: Vec<String>,
116    },
117
118    /// YouTube is blocking requests from your IP address
119    IpBlocked(Option<Box<dyn ProxyConfig>>),
120
121    /// YouTube is blocking your request (rate limiting, etc.)
122    RequestBlocked(Option<Box<dyn ProxyConfig>>),
123
124    /// The requested transcript cannot be translated with specific error details
125    TranslationUnavailable(String),
126
127    /// The requested translation language is not available with specific error details
128    TranslationLanguageUnavailable(String),
129
130    /// Failed to create a consent cookie required by YouTube
131    FailedToCreateConsentCookie,
132
133    /// The request to YouTube failed with a specific error
134    YouTubeRequestFailed(String),
135
136    /// The provided video ID is invalid
137    InvalidVideoId,
138
139    /// The video is age-restricted and requires authentication
140    AgeRestricted,
141
142    /// The YouTube data structure couldn't be parsed
143    YouTubeDataUnparsable(String),
144}
145
146impl CouldNotRetrieveTranscript {
147    /// Builds a detailed error message based on the error reason
148    fn build_error_message(&self) -> String {
149        let base_error = format!(
150            "Could not retrieve a transcript for the video {}!",
151            self.video_id.replace("{video_id}", &self.video_id)
152        );
153
154        match &self.reason {
155            Some(reason) => {
156                let cause = match reason {
157                    CouldNotRetrieveTranscriptReason::TranscriptsDisabled => {
158                        "Subtitles are disabled for this video".to_string()
159                    },
160                    CouldNotRetrieveTranscriptReason::NoTranscriptFound { requested_language_codes, transcript_data } => {
161                        format!("No transcripts were found for any of the requested language codes: {:?}\n\n{}", 
162                            requested_language_codes, transcript_data)
163                    },
164                    CouldNotRetrieveTranscriptReason::VideoUnavailable => {
165                        "The video is no longer available".to_string()
166                    },
167                    CouldNotRetrieveTranscriptReason::VideoUnplayable { reason, sub_reasons } => {
168                        let reason_str = reason.clone().unwrap_or_else(|| "No reason specified!".to_string());
169                        let mut message = format!("The video is unplayable for the following reason: {}", reason_str);
170                        if !sub_reasons.is_empty() {
171                            message.push_str("\n\nAdditional Details:\n");
172                            for sub_reason in sub_reasons {
173                                message.push_str(&format!(" - {}\n", sub_reason));
174                            }
175                        }
176                        message
177                    },
178                    CouldNotRetrieveTranscriptReason::IpBlocked(proxy_config) => {
179                        let base_cause = "YouTube is blocking requests from your IP. This usually is due to one of the following reasons:
180- You have done too many requests and your IP has been blocked by YouTube
181- You are doing requests from an IP belonging to a cloud provider (like AWS, Google Cloud Platform, Azure, etc.). Unfortunately, most IPs from cloud providers are blocked by YouTube.";
182                        match proxy_config {
183                            Some(config) if config.as_any().is::<WebshareProxyConfig>() => {
184                                format!("{}\n\nYouTube is blocking your requests, despite you using Webshare proxies. Please make sure that you have purchased \"Residential\" proxies and NOT \"Proxy Server\" or \"Static Residential\", as those won't work as reliably! The free tier also uses \"Proxy Server\" and will NOT work!\n\nThe only reliable option is using \"Residential\" proxies (not \"Static Residential\"), as this allows you to rotate through a pool of over 30M IPs, which means you will always find an IP that hasn't been blocked by YouTube yet!", base_cause)
185                            },
186                            Some(config) if config.as_any().is::<GenericProxyConfig>() => {
187                                format!("{}\n\nYouTube is blocking your requests, despite you using proxies. Keep in mind a proxy is just a way to hide your real IP behind the IP of that proxy, but there is no guarantee that the IP of that proxy won't be blocked as well.\n\nThe only truly reliable way to prevent IP blocks is rotating through a large pool of residential IPs, by using a provider like Webshare.", base_cause)
188                            },
189                            _ => {
190                                format!("{}\n\nIp blocked.", base_cause)
191                            }
192                        }
193                    },
194                    CouldNotRetrieveTranscriptReason::RequestBlocked(proxy_config) => {
195                        let base_cause = "YouTube is blocking requests from your IP. This usually is due to one of the following reasons:
196- You have done too many requests and your IP has been blocked by YouTube
197- You are doing requests from an IP belonging to a cloud provider (like AWS, Google Cloud Platform, Azure, etc.). Unfortunately, most IPs from cloud providers are blocked by YouTube.";
198                        match proxy_config {
199                            Some(config) if config.as_any().is::<WebshareProxyConfig>() => {
200                                format!("{}\n\nYouTube is blocking your requests, despite you using Webshare proxies. Please make sure that you have purchased \"Residential\" proxies and NOT \"Proxy Server\" or \"Static Residential\", as those won't work as reliably! The free tier also uses \"Proxy Server\" and will NOT work!\n\nThe only reliable option is using \"Residential\" proxies (not \"Static Residential\"), as this allows you to rotate through a pool of over 30M IPs, which means you will always find an IP that hasn't been blocked by YouTube yet!", base_cause)
201                            },
202                            Some(config) if config.as_any().is::<GenericProxyConfig>() => {
203                                format!("{}\n\nYouTube is blocking your requests, despite you using proxies. Keep in mind a proxy is just a way to hide your real IP behind the IP of that proxy, but there is no guarantee that the IP of that proxy won't be blocked as well.\n\nThe only truly reliable way to prevent IP blocks is rotating through a large pool of residential IPs, by using a provider like Webshare.", base_cause)
204                            },
205                            _ => {
206                                format!("{}\n\nRequest blocked.", base_cause)
207                            }
208                        }
209                    },
210                    CouldNotRetrieveTranscriptReason::TranslationUnavailable(message) => {
211                        format!("The requested transcript cannot be translated: {}", message)
212                    },
213                    CouldNotRetrieveTranscriptReason::TranslationLanguageUnavailable(message) => {
214                        format!("The requested translation language is not available: {}", message)
215                    },
216                    CouldNotRetrieveTranscriptReason::FailedToCreateConsentCookie => {
217                        "Failed to automatically give consent to saving cookies".to_string()
218                    },
219                    CouldNotRetrieveTranscriptReason::YouTubeRequestFailed(error) => {
220                        format!("Failed to make a request to YouTube. Error: {}", error)
221                    },
222                    CouldNotRetrieveTranscriptReason::InvalidVideoId => {
223                        "You provided an invalid video id. Make sure you are using the video id and NOT the url!`".to_string()
224                    },
225                    CouldNotRetrieveTranscriptReason::AgeRestricted => {
226                        "This video is age-restricted. Therefore, you will have to authenticate to be able to retrieve transcripts for it. You will have to provide a cookie to authenticate yourself.".to_string()
227                    },
228                    CouldNotRetrieveTranscriptReason::YouTubeDataUnparsable(details) => {
229                        format!("The data required to fetch the transcript is not parsable: {}. This should not happen, please open an issue (make sure to include the video ID)!", details)
230                    },
231                };
232
233                format!("{} This is most likely caused by:\n\n{}", base_error, cause)
234            }
235            None => base_error,
236        }
237    }
238}
239
240/// Type alias for when transcripts are disabled for a video
241pub type TranscriptsDisabled = CouldNotRetrieveTranscript;
242
243/// Type alias for when no transcript is found in the requested languages
244pub type NoTranscriptFound = CouldNotRetrieveTranscript;
245
246/// Type alias for when the video is no longer available
247pub type VideoUnavailable = CouldNotRetrieveTranscript;
248
249/// Type alias for when the video cannot be played
250pub type VideoUnplayable = CouldNotRetrieveTranscript;
251
252/// Type alias for when YouTube is blocking your IP address
253pub type IpBlocked = CouldNotRetrieveTranscript;
254
255/// Type alias for when YouTube is blocking your request
256pub type RequestBlocked = CouldNotRetrieveTranscript;
257
258/// Type alias for when the requested transcript cannot be translated
259pub type NotTranslatable = CouldNotRetrieveTranscript;
260
261/// Type alias for when the requested translation language is not available
262pub type TranslationLanguageNotAvailable = CouldNotRetrieveTranscript;
263
264/// Type alias for when creating a consent cookie fails
265pub type FailedToCreateConsentCookie = CouldNotRetrieveTranscript;
266
267/// Type alias for when a request to YouTube fails
268pub type YouTubeRequestFailed = CouldNotRetrieveTranscript;
269
270/// Type alias for when an invalid video ID is provided
271pub type InvalidVideoId = CouldNotRetrieveTranscript;
272
273/// Type alias for when the video is age-restricted and requires authentication
274pub type AgeRestricted = CouldNotRetrieveTranscript;
275
276/// Type alias for when YouTube data cannot be parsed
277pub type YouTubeDataUnparsable = CouldNotRetrieveTranscript;
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use std::any::Any;
283    use std::collections::HashMap;
284
285    // Mock implementation for testing
286    #[derive(Debug)]
287    struct MockProxy;
288
289    impl ProxyConfig for MockProxy {
290        fn to_requests_dict(&self) -> HashMap<String, String> {
291            HashMap::new()
292        }
293
294        fn as_any(&self) -> &dyn Any {
295            self
296        }
297    }
298
299    #[test]
300    fn test_build_error_message_no_reason() {
301        let error = CouldNotRetrieveTranscript {
302            video_id: "dQw4w9WgXcQ".to_string(),
303            reason: None,
304        };
305
306        let message = error.build_error_message();
307        assert!(message.contains("Could not retrieve a transcript"));
308        assert!(message.contains("dQw4w9WgXcQ"));
309        // Should be a simple message without additional cause
310        assert!(!message.contains("This is most likely caused by"));
311    }
312
313    #[test]
314    fn test_build_error_message_transcripts_disabled() {
315        let error = CouldNotRetrieveTranscript {
316            video_id: "dQw4w9WgXcQ".to_string(),
317            reason: Some(CouldNotRetrieveTranscriptReason::TranscriptsDisabled),
318        };
319
320        let message = error.build_error_message();
321        assert!(message.contains("Could not retrieve a transcript"));
322        assert!(message.contains("Subtitles are disabled"));
323    }
324
325    #[test]
326    fn test_build_error_message_no_transcript_found() {
327        let transcript_list = TranscriptList {
328            video_id: "dQw4w9WgXcQ".to_string(),
329            manually_created_transcripts: HashMap::new(),
330            generated_transcripts: HashMap::new(),
331            translation_languages: vec![],
332        };
333
334        let error = CouldNotRetrieveTranscript {
335            video_id: "dQw4w9WgXcQ".to_string(),
336            reason: Some(CouldNotRetrieveTranscriptReason::NoTranscriptFound {
337                requested_language_codes: vec!["fr".to_string(), "es".to_string()],
338                transcript_data: transcript_list,
339            }),
340        };
341
342        let message = error.build_error_message();
343        assert!(message.contains("Could not retrieve a transcript"));
344        assert!(message.contains("No transcripts were found"));
345        assert!(message.contains("fr"));
346        assert!(message.contains("es"));
347    }
348
349    #[test]
350    fn test_build_error_message_video_unavailable() {
351        let error = CouldNotRetrieveTranscript {
352            video_id: "dQw4w9WgXcQ".to_string(),
353            reason: Some(CouldNotRetrieveTranscriptReason::VideoUnavailable),
354        };
355
356        let message = error.build_error_message();
357        assert!(message.contains("Could not retrieve a transcript"));
358        assert!(message.contains("video is no longer available"));
359    }
360
361    #[test]
362    fn test_build_error_message_video_unplayable() {
363        // Test with reason and sub-reasons
364        let error = CouldNotRetrieveTranscript {
365            video_id: "dQw4w9WgXcQ".to_string(),
366            reason: Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
367                reason: Some("Content is private".to_string()),
368                sub_reasons: vec![
369                    "The owner has made this content private".to_string(),
370                    "You need permission to access".to_string(),
371                ],
372            }),
373        };
374
375        let message = error.build_error_message();
376        assert!(message.contains("Could not retrieve a transcript"));
377        assert!(message.contains("video is unplayable"));
378        assert!(message.contains("Content is private"));
379        assert!(message.contains("The owner has made this content private"));
380        assert!(message.contains("You need permission to access"));
381
382        // Test with no reason (just sub-reasons)
383        let error = CouldNotRetrieveTranscript {
384            video_id: "dQw4w9WgXcQ".to_string(),
385            reason: Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
386                reason: None,
387                sub_reasons: vec!["Region restricted".to_string()],
388            }),
389        };
390
391        let message = error.build_error_message();
392        assert!(message.contains("No reason specified"));
393        assert!(message.contains("Region restricted"));
394
395        // Test with reason but no sub-reasons
396        let error = CouldNotRetrieveTranscript {
397            video_id: "dQw4w9WgXcQ".to_string(),
398            reason: Some(CouldNotRetrieveTranscriptReason::VideoUnplayable {
399                reason: Some("Premium content".to_string()),
400                sub_reasons: vec![],
401            }),
402        };
403
404        let message = error.build_error_message();
405        assert!(message.contains("Premium content"));
406        assert!(!message.contains("Additional Details"));
407    }
408
409    #[test]
410    fn test_build_error_message_ip_blocked() {
411        // Without proxy config
412        let error = CouldNotRetrieveTranscript {
413            video_id: "dQw4w9WgXcQ".to_string(),
414            reason: Some(CouldNotRetrieveTranscriptReason::IpBlocked(None)),
415        };
416
417        let message = error.build_error_message();
418        assert!(message.contains("Could not retrieve a transcript"));
419        assert!(message.contains("YouTube is blocking requests from your IP"));
420        assert!(message.contains("Ip blocked"));
421
422        // With MockProxy
423        let mock_proxy = Box::new(MockProxy);
424        let error = CouldNotRetrieveTranscript {
425            video_id: "dQw4w9WgXcQ".to_string(),
426            reason: Some(CouldNotRetrieveTranscriptReason::IpBlocked(Some(
427                mock_proxy,
428            ))),
429        };
430
431        let message = error.build_error_message();
432        assert!(message.contains("Could not retrieve a transcript"));
433        assert!(message.contains("YouTube is blocking requests from your IP"));
434    }
435
436    #[test]
437    fn test_build_error_message_request_blocked() {
438        // Without proxy config
439        let error = CouldNotRetrieveTranscript {
440            video_id: "dQw4w9WgXcQ".to_string(),
441            reason: Some(CouldNotRetrieveTranscriptReason::RequestBlocked(None)),
442        };
443
444        let message = error.build_error_message();
445        assert!(message.contains("Could not retrieve a transcript"));
446        assert!(message.contains("YouTube is blocking requests from your IP"));
447        assert!(message.contains("Request blocked"));
448
449        // With MockProxy
450        let mock_proxy = Box::new(MockProxy);
451        let error = CouldNotRetrieveTranscript {
452            video_id: "dQw4w9WgXcQ".to_string(),
453            reason: Some(CouldNotRetrieveTranscriptReason::RequestBlocked(Some(
454                mock_proxy,
455            ))),
456        };
457
458        let message = error.build_error_message();
459        assert!(message.contains("Could not retrieve a transcript"));
460        assert!(message.contains("YouTube is blocking requests from your IP"));
461    }
462
463    #[test]
464    fn test_build_error_message_translation_errors() {
465        // TranslationUnavailable
466        let error = CouldNotRetrieveTranscript {
467            video_id: "dQw4w9WgXcQ".to_string(),
468            reason: Some(CouldNotRetrieveTranscriptReason::TranslationUnavailable(
469                "Manual transcripts cannot be translated".to_string(),
470            )),
471        };
472
473        let message = error.build_error_message();
474        assert!(message.contains("Could not retrieve a transcript"));
475        assert!(message.contains("transcript cannot be translated"));
476        assert!(message.contains("Manual transcripts cannot be translated"));
477
478        // TranslationLanguageUnavailable
479        let error = CouldNotRetrieveTranscript {
480            video_id: "dQw4w9WgXcQ".to_string(),
481            reason: Some(
482                CouldNotRetrieveTranscriptReason::TranslationLanguageUnavailable(
483                    "Klingon is not supported".to_string(),
484                ),
485            ),
486        };
487
488        let message = error.build_error_message();
489        assert!(message.contains("Could not retrieve a transcript"));
490        assert!(message.contains("translation language is not available"));
491        assert!(message.contains("Klingon is not supported"));
492    }
493
494    #[test]
495    fn test_build_error_message_misc_errors() {
496        // FailedToCreateConsentCookie
497        let error = CouldNotRetrieveTranscript {
498            video_id: "dQw4w9WgXcQ".to_string(),
499            reason: Some(CouldNotRetrieveTranscriptReason::FailedToCreateConsentCookie),
500        };
501
502        let message = error.build_error_message();
503        assert!(message.contains("Could not retrieve a transcript"));
504        assert!(message.contains("Failed to automatically give consent"));
505
506        // YouTubeRequestFailed
507        let error = CouldNotRetrieveTranscript {
508            video_id: "dQw4w9WgXcQ".to_string(),
509            reason: Some(CouldNotRetrieveTranscriptReason::YouTubeRequestFailed(
510                "Connection timed out".to_string(),
511            )),
512        };
513
514        let message = error.build_error_message();
515        assert!(message.contains("Could not retrieve a transcript"));
516        assert!(message.contains("Failed to make a request to YouTube"));
517        assert!(message.contains("Connection timed out"));
518
519        // InvalidVideoId
520        let error = CouldNotRetrieveTranscript {
521            video_id: "invalid".to_string(),
522            reason: Some(CouldNotRetrieveTranscriptReason::InvalidVideoId),
523        };
524
525        let message = error.build_error_message();
526        assert!(message.contains("Could not retrieve a transcript"));
527        assert!(message.contains("invalid video id"));
528
529        // AgeRestricted
530        let error = CouldNotRetrieveTranscript {
531            video_id: "dQw4w9WgXcQ".to_string(),
532            reason: Some(CouldNotRetrieveTranscriptReason::AgeRestricted),
533        };
534
535        let message = error.build_error_message();
536        assert!(message.contains("Could not retrieve a transcript"));
537        assert!(message.contains("age-restricted"));
538        assert!(message.contains("authenticate"));
539
540        // YouTubeDataUnparsable
541        let error = CouldNotRetrieveTranscript {
542            video_id: "dQw4w9WgXcQ".to_string(),
543            reason: Some(CouldNotRetrieveTranscriptReason::YouTubeDataUnparsable(
544                "Invalid XML format".to_string(),
545            )),
546        };
547
548        let message = error.build_error_message();
549        assert!(message.contains("Could not retrieve a transcript"));
550        assert!(message.contains("not parsable"));
551        assert!(message.contains("open an issue"));
552    }
553}