Skip to main content

#
jellytau_lib/thumbnail/
worker.rs

    1//! Thumbnail download worker
    2
    3use std::time::Duration;
    4
    5/// Worker for downloading thumbnails from Jellyfin server
    6pub struct ThumbnailWorker {
    7    client: reqwest::Client,
    8}
    9
    10impl ThumbnailWorker {
    11    /// Create a new thumbnail worker
    12    pub fn new() -> Self {
    13        let client = reqwest::Client::builder()
    14            .timeout(Duration::from_secs(30))
    15            .https_only(true)
    16            .build()
    17            .expect("Failed to create HTTP client");
    18
    19        Self { client }
    20    }
    21
    22    /// Download a thumbnail from URL
    23    pub async fn download(&self, url: &str) -> Result<Vec<u8>, ThumbnailDownloadError> {
    24        let response = self
    25            .client
    26            .get(url)
    27            .send()
    28            .await
    29            .map_err(|e| ThumbnailDownloadError::Network(e.to_string()))?;
    30
    31        let status = response.status();
    32        if !status.is_success() {
    33            return Err(ThumbnailDownloadError::Http(status.as_u16()));
    34        }
    35
    36        let bytes = response
    37            .bytes()
    38            .await
    39            .map_err(|e| ThumbnailDownloadError::Network(e.to_string()))?;
    40
    41        Ok(bytes.to_vec())
    42    }
    43
    44    /// Download with retry logic
    45    pub async fn download_with_retry(
    46        &self,
    47        url: &str,
    48        max_retries: u32,
    49    ) -> Result<Vec<u8>, ThumbnailDownloadError> {
    50        let mut last_error = ThumbnailDownloadError::Network("No attempts made".to_string());
    51
    52        for attempt in 0..=max_retries {
    53            match self.download(url).await {
    54                Ok(data) => return Ok(data),
    55                Err(e) if e.is_retryable() && attempt < max_retries => {
    56                    last_error = e;
    57                    // Simple exponential backoff: 100ms, 200ms, 400ms
    58                    let delay = Duration::from_millis(100 * (1 << attempt));
    59                    tokio::time::sleep(delay).await;
    60                }
    61                Err(e) => return Err(e),
    62            }
    63        }
    64
    65        Err(last_error)
    66    }
    67}
    68
    69impl Default for ThumbnailWorker {
    70    fn default() -> Self {
    71        Self::new()
    72    }
    73}
    74
    75/// Errors that can occur during thumbnail download
    76#[derive(Debug)]
    77pub enum ThumbnailDownloadError {
    78    Network(String),
    79    Http(u16),
    80}
    81
    82impl ThumbnailDownloadError {
    83    /// Check if this error is retryable
    84    pub fn is_retryable(&self) -> bool {
    85        match self {
    86            ThumbnailDownloadError::Network(_) => true,
    87            ThumbnailDownloadError::Http(status) => *status >= 500,
    88        }
    89    }
    90}
    91
    92impl std::fmt::Display for ThumbnailDownloadError {
    93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    94        match self {
    95            ThumbnailDownloadError::Network(msg) => write!(f, "Network error: {}", msg),
    96            ThumbnailDownloadError::Http(status) => write!(f, "HTTP error: {}", status),
    97        }
    98    }
    99}
    100
    101impl std::error::Error for ThumbnailDownloadError {}
    102
    103#[cfg(test)]
    104mod tests {
    105    use super::*;
    106
    107    #[test]
    108    fn test_error_retryable() {
    109        assert!(ThumbnailDownloadError::Network("timeout".to_string()).is_retryable());
    110        assert!(ThumbnailDownloadError::Http(500).is_retryable());
    111        assert!(ThumbnailDownloadError::Http(503).is_retryable());
    112        assert!(!ThumbnailDownloadError::Http(404).is_retryable());
    113        assert!(!ThumbnailDownloadError::Http(400).is_retryable());
    114    }
    115
    116    #[test]
    117    fn test_worker_creation() {
    118        let _worker = ThumbnailWorker::new();
    119        // Just verify it doesn't panic
    120        let _default = ThumbnailWorker::default();
    121    }
    122}