ydl/
error.rs

1use thiserror::Error;
2
3/// Main error type for the Ydl API
4#[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    /// Check if the error is retryable
99    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    /// Get suggested retry delay in seconds
110    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    /// Check if error indicates the video content is inaccessible
121    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    /// Check if error indicates subtitle availability issues
132    pub fn is_subtitle_unavailable(&self) -> bool {
133        matches!(
134            self,
135            YdlError::NoSubtitlesAvailable { .. }
136                | YdlError::OnlyAutoGenerated { .. }
137                | YdlError::LanguageNotAvailable { .. }
138        )
139    }
140}
141
142/// Result type alias for YdlError
143pub 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        // Create a network error without relying on conversion
152        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}