1use crate::proxies::{GenericProxyConfig, ProxyConfig, WebshareProxyConfig};
2use crate::TranscriptList;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
12pub enum YouTubeTranscriptApiError {
13 #[error("YouTube Transcript API error")]
14 Generic,
15}
16
17#[derive(Debug, Error)]
24pub enum CookieError {
25 #[error("Cookie error")]
26 Generic,
27
28 #[error("Can't load the provided cookie file: {0}")]
30 PathInvalid(String),
31
32 #[error("The cookies provided are not valid (may have expired): {0}")]
34 Invalid(String),
35}
36
37pub type CookiePathInvalid = CookieError;
39
40pub type CookieInvalid = CookieError;
42
43#[derive(Debug, Error)]
77#[error("{}", self.build_error_message())]
78pub struct CouldNotRetrieveTranscript {
79 pub video_id: String,
81
82 pub reason: Option<CouldNotRetrieveTranscriptReason>,
84}
85
86#[derive(Debug)]
93pub enum CouldNotRetrieveTranscriptReason {
94 TranscriptsDisabled,
96
97 NoTranscriptFound {
99 requested_language_codes: Vec<String>,
101
102 transcript_data: TranscriptList,
104 },
105
106 VideoUnavailable,
108
109 VideoUnplayable {
111 reason: Option<String>,
113
114 sub_reasons: Vec<String>,
116 },
117
118 IpBlocked(Option<Box<dyn ProxyConfig>>),
120
121 RequestBlocked(Option<Box<dyn ProxyConfig>>),
123
124 TranslationUnavailable(String),
126
127 TranslationLanguageUnavailable(String),
129
130 FailedToCreateConsentCookie,
132
133 YouTubeRequestFailed(String),
135
136 InvalidVideoId,
138
139 AgeRestricted,
141
142 YouTubeDataUnparsable(String),
144}
145
146impl CouldNotRetrieveTranscript {
147 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
240pub type TranscriptsDisabled = CouldNotRetrieveTranscript;
242
243pub type NoTranscriptFound = CouldNotRetrieveTranscript;
245
246pub type VideoUnavailable = CouldNotRetrieveTranscript;
248
249pub type VideoUnplayable = CouldNotRetrieveTranscript;
251
252pub type IpBlocked = CouldNotRetrieveTranscript;
254
255pub type RequestBlocked = CouldNotRetrieveTranscript;
257
258pub type NotTranslatable = CouldNotRetrieveTranscript;
260
261pub type TranslationLanguageNotAvailable = CouldNotRetrieveTranscript;
263
264pub type FailedToCreateConsentCookie = CouldNotRetrieveTranscript;
266
267pub type YouTubeRequestFailed = CouldNotRetrieveTranscript;
269
270pub type InvalidVideoId = CouldNotRetrieveTranscript;
272
273pub type AgeRestricted = CouldNotRetrieveTranscript;
275
276pub type YouTubeDataUnparsable = CouldNotRetrieveTranscript;
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::any::Any;
283 use std::collections::HashMap;
284
285 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}