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}