ydl-lib 0.2.0

A fast, reliable YouTube subtitle downloader library written in Rust
Documentation
use thiserror::Error;

/// Main error type for the Ydl API
#[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 {
    /// Check if the error is retryable
    pub fn is_retryable(&self) -> bool {
        matches!(
            self,
            YdlError::Network { .. }
                | YdlError::RateLimited { .. }
                | YdlError::Timeout { .. }
                | YdlError::ServiceUnavailable
        )
    }

    /// Get suggested retry delay in seconds
    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,
        }
    }

    /// Check if error indicates the video content is inaccessible
    pub fn is_video_inaccessible(&self) -> bool {
        matches!(
            self,
            YdlError::VideoNotFound { .. }
                | YdlError::VideoRestricted { .. }
                | YdlError::GeoBlocked { .. }
                | YdlError::AgeRestricted { .. }
        )
    }

    /// Check if error indicates subtitle availability issues
    pub fn is_subtitle_unavailable(&self) -> bool {
        matches!(
            self,
            YdlError::NoSubtitlesAvailable { .. }
                | YdlError::OnlyAutoGenerated { .. }
                | YdlError::LanguageNotAvailable { .. }
        )
    }
}

/// Result type alias for YdlError
pub type YdlResult<T> = Result<T, YdlError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_is_retryable() {
        // Create a network error without relying on conversion
        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());
    }
}