1use thiserror::Error;
2
3#[derive(Error, Debug)]
5pub enum YdlError {
6 #[error("Invalid YouTube URL format: {url}")]
7 InvalidUrl { url: String },
8
9 #[error("Invalid video ID format: {video_id}")]
10 InvalidVideoId { video_id: String },
11
12 #[error("Network error: {source}")]
13 Network {
14 #[from]
15 source: reqwest::Error,
16 },
17
18 #[error("Video not found or unavailable: {video_id}")]
19 VideoNotFound { video_id: String },
20
21 #[error("Video is private or restricted: {video_id}")]
22 VideoRestricted { video_id: String },
23
24 #[error("Content is geo-blocked in this region: {video_id}")]
25 GeoBlocked { video_id: String },
26
27 #[error("Age-restricted content requires verification: {video_id}")]
28 AgeRestricted { video_id: String },
29
30 #[error("No subtitles available for video: {video_id}")]
31 NoSubtitlesAvailable { video_id: String },
32
33 #[error("Only auto-generated subtitles available for video: {video_id}")]
34 OnlyAutoGenerated { video_id: String },
35
36 #[error("Requested language not available: {language}")]
37 LanguageNotAvailable { language: String },
38
39 #[error("Unsupported subtitle format: {format}")]
40 UnsupportedFormat { format: String },
41
42 #[error("Failed to parse video metadata: {message}")]
43 MetadataParsingError { message: String },
44
45 #[error("Failed to discover subtitle tracks: {message}")]
46 SubtitleDiscoveryError { message: String },
47
48 #[error("File system error: {source}")]
49 FileSystem {
50 #[from]
51 source: std::io::Error,
52 },
53
54 #[error("Subtitle parsing error: {message}")]
55 SubtitleParsing { message: String },
56
57 #[error("Format conversion error: from {from} to {to}")]
58 FormatConversion { from: String, to: String },
59
60 #[error("Rate limit exceeded, retry after {retry_after}s")]
61 RateLimited { retry_after: u64 },
62
63 #[error("Request timeout after {timeout}s")]
64 Timeout { timeout: u64 },
65
66 #[error("YouTube service temporarily unavailable")]
67 ServiceUnavailable,
68
69 #[error("Configuration error: {message}")]
70 Configuration { message: String },
71
72 #[error("Processing error: {message}")]
73 Processing { message: String },
74
75 #[error("JSON parsing error: {source}")]
76 JsonParsing {
77 #[from]
78 source: serde_json::Error,
79 },
80
81 #[error("URL parsing error: {source}")]
82 UrlParsing {
83 #[from]
84 source: url::ParseError,
85 },
86
87 #[error("Regex error: {source}")]
88 Regex {
89 #[from]
90 source: regex::Error,
91 },
92
93 #[error("Encoding error: {message}")]
94 Encoding { message: String },
95}
96
97impl YdlError {
98 pub fn is_retryable(&self) -> bool {
100 matches!(
101 self,
102 YdlError::Network { .. }
103 | YdlError::RateLimited { .. }
104 | YdlError::Timeout { .. }
105 | YdlError::ServiceUnavailable
106 )
107 }
108
109 pub fn retry_delay(&self) -> Option<u64> {
111 match self {
112 YdlError::RateLimited { retry_after } => Some(*retry_after),
113 YdlError::Network { .. } => Some(1),
114 YdlError::ServiceUnavailable => Some(5),
115 YdlError::Timeout { .. } => Some(2),
116 _ => None,
117 }
118 }
119
120 pub fn is_video_inaccessible(&self) -> bool {
122 matches!(
123 self,
124 YdlError::VideoNotFound { .. }
125 | YdlError::VideoRestricted { .. }
126 | YdlError::GeoBlocked { .. }
127 | YdlError::AgeRestricted { .. }
128 )
129 }
130
131 pub fn is_subtitle_unavailable(&self) -> bool {
133 matches!(
134 self,
135 YdlError::NoSubtitlesAvailable { .. }
136 | YdlError::OnlyAutoGenerated { .. }
137 | YdlError::LanguageNotAvailable { .. }
138 )
139 }
140}
141
142pub type YdlResult<T> = Result<T, YdlError>;
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_error_is_retryable() {
151 let rate_limit_err = YdlError::RateLimited { retry_after: 60 };
153 assert!(rate_limit_err.is_retryable());
154
155 let rate_limit_err = YdlError::RateLimited { retry_after: 60 };
156 assert!(rate_limit_err.is_retryable());
157
158 let invalid_url_err = YdlError::InvalidUrl {
159 url: "not-a-url".to_string(),
160 };
161 assert!(!invalid_url_err.is_retryable());
162 }
163
164 #[test]
165 fn test_retry_delay() {
166 let rate_limit_err = YdlError::RateLimited { retry_after: 30 };
167 assert_eq!(rate_limit_err.retry_delay(), Some(30));
168
169 let service_err = YdlError::ServiceUnavailable;
170 assert_eq!(service_err.retry_delay(), Some(5));
171
172 let invalid_url_err = YdlError::InvalidUrl {
173 url: "not-a-url".to_string(),
174 };
175 assert_eq!(invalid_url_err.retry_delay(), None);
176 }
177
178 #[test]
179 fn test_video_inaccessible() {
180 let not_found_err = YdlError::VideoNotFound {
181 video_id: "test123".to_string(),
182 };
183 assert!(not_found_err.is_video_inaccessible());
184
185 let restricted_err = YdlError::VideoRestricted {
186 video_id: "test123".to_string(),
187 };
188 assert!(restricted_err.is_video_inaccessible());
189
190 let network_err = YdlError::ServiceUnavailable;
191 assert!(!network_err.is_video_inaccessible());
192 }
193
194 #[test]
195 fn test_subtitle_unavailable() {
196 let no_subs_err = YdlError::NoSubtitlesAvailable {
197 video_id: "test123".to_string(),
198 };
199 assert!(no_subs_err.is_subtitle_unavailable());
200
201 let auto_gen_err = YdlError::OnlyAutoGenerated {
202 video_id: "test123".to_string(),
203 };
204 assert!(auto_gen_err.is_subtitle_unavailable());
205
206 let network_err = YdlError::ServiceUnavailable;
207 assert!(!network_err.is_subtitle_unavailable());
208 }
209}