use thiserror::Error;
#[derive(Error, Debug)]
pub enum YdlError {
#[error("Invalid YouTube URL format: {url}")]
InvalidUrl { url: String },
#[error("Invalid video ID format: {video_id}")]
InvalidVideoId { video_id: String },
#[error("Network error: {source}")]
Network {
#[from]
source: reqwest::Error,
},
#[error("Video not found or unavailable: {video_id}")]
VideoNotFound { video_id: String },
#[error("Video is private or restricted: {video_id}")]
VideoRestricted { video_id: String },
#[error("Content is geo-blocked in this region: {video_id}")]
GeoBlocked { video_id: String },
#[error("Age-restricted content requires verification: {video_id}")]
AgeRestricted { video_id: String },
#[error("No subtitles available for video: {video_id}")]
NoSubtitlesAvailable { video_id: String },
#[error("Only auto-generated subtitles available for video: {video_id}")]
OnlyAutoGenerated { video_id: String },
#[error("Requested language not available: {language}")]
LanguageNotAvailable { language: String },
#[error("Unsupported subtitle format: {format}")]
UnsupportedFormat { format: String },
#[error("Failed to parse video metadata: {message}")]
MetadataParsingError { message: String },
#[error("Failed to discover subtitle tracks: {message}")]
SubtitleDiscoveryError { message: String },
#[error("File system error: {source}")]
FileSystem {
#[from]
source: std::io::Error,
},
#[error("Subtitle parsing error: {message}")]
SubtitleParsing { message: String },
#[error("Format conversion error: from {from} to {to}")]
FormatConversion { from: String, to: String },
#[error("Rate limit exceeded, retry after {retry_after}s")]
RateLimited { retry_after: u64 },
#[error("Request timeout after {timeout}s")]
Timeout { timeout: u64 },
#[error("YouTube service temporarily unavailable")]
ServiceUnavailable,
#[error("Configuration error: {message}")]
Configuration { message: String },
#[error("Processing error: {message}")]
Processing { message: String },
#[error("JSON parsing error: {source}")]
JsonParsing {
#[from]
source: serde_json::Error,
},
#[error("URL parsing error: {source}")]
UrlParsing {
#[from]
source: url::ParseError,
},
#[error("Regex error: {source}")]
Regex {
#[from]
source: regex::Error,
},
#[error("Encoding error: {message}")]
Encoding { message: String },
}
impl YdlError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
YdlError::Network { .. }
| YdlError::RateLimited { .. }
| YdlError::Timeout { .. }
| YdlError::ServiceUnavailable
)
}
pub fn retry_delay(&self) -> Option<u64> {
match self {
YdlError::RateLimited { retry_after } => Some(*retry_after),
YdlError::Network { .. } => Some(1),
YdlError::ServiceUnavailable => Some(5),
YdlError::Timeout { .. } => Some(2),
_ => None,
}
}
pub fn is_video_inaccessible(&self) -> bool {
matches!(
self,
YdlError::VideoNotFound { .. }
| YdlError::VideoRestricted { .. }
| YdlError::GeoBlocked { .. }
| YdlError::AgeRestricted { .. }
)
}
pub fn is_subtitle_unavailable(&self) -> bool {
matches!(
self,
YdlError::NoSubtitlesAvailable { .. }
| YdlError::OnlyAutoGenerated { .. }
| YdlError::LanguageNotAvailable { .. }
)
}
}
pub type YdlResult<T> = Result<T, YdlError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_is_retryable() {
let rate_limit_err = YdlError::RateLimited { retry_after: 60 };
assert!(rate_limit_err.is_retryable());
let rate_limit_err = YdlError::RateLimited { retry_after: 60 };
assert!(rate_limit_err.is_retryable());
let invalid_url_err = YdlError::InvalidUrl {
url: "not-a-url".to_string(),
};
assert!(!invalid_url_err.is_retryable());
}
#[test]
fn test_retry_delay() {
let rate_limit_err = YdlError::RateLimited { retry_after: 30 };
assert_eq!(rate_limit_err.retry_delay(), Some(30));
let service_err = YdlError::ServiceUnavailable;
assert_eq!(service_err.retry_delay(), Some(5));
let invalid_url_err = YdlError::InvalidUrl {
url: "not-a-url".to_string(),
};
assert_eq!(invalid_url_err.retry_delay(), None);
}
#[test]
fn test_video_inaccessible() {
let not_found_err = YdlError::VideoNotFound {
video_id: "test123".to_string(),
};
assert!(not_found_err.is_video_inaccessible());
let restricted_err = YdlError::VideoRestricted {
video_id: "test123".to_string(),
};
assert!(restricted_err.is_video_inaccessible());
let network_err = YdlError::ServiceUnavailable;
assert!(!network_err.is_video_inaccessible());
}
#[test]
fn test_subtitle_unavailable() {
let no_subs_err = YdlError::NoSubtitlesAvailable {
video_id: "test123".to_string(),
};
assert!(no_subs_err.is_subtitle_unavailable());
let auto_gen_err = YdlError::OnlyAutoGenerated {
video_id: "test123".to_string(),
};
assert!(auto_gen_err.is_subtitle_unavailable());
let network_err = YdlError::ServiceUnavailable;
assert!(!network_err.is_subtitle_unavailable());
}
}