Skip to main content

pexels_api/
download.rs

1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3use std::time::Duration;
4
5use futures::stream::StreamExt;
6use reqwest::header::HeaderMap;
7use reqwest::{header, Client};
8use tokio::fs;
9use tokio::io::AsyncWriteExt;
10use tokio::sync::Semaphore;
11
12use crate::models::{Photo, Video};
13use crate::PexelsError;
14
15/// Picture quality enumeration
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ImageQuality {
18    Original,
19    Large2x,
20    Large,
21    Medium,
22    Small,
23    Portrait,
24    Landscape,
25    Tiny,
26}
27
28/// Video quality enumeration
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum VideoQuality {
31    HD,
32    SD,
33    Tiny,
34}
35
36/// The type of progress callback function
37pub type ProgressCallback = fn(current: u64, total: u64);
38
39/// Result type alias
40type Result<T> = std::result::Result<T, PexelsError>;
41
42pub struct DownloadManager {
43    client: Client,
44    max_concurrent: usize,
45}
46
47impl DownloadManager {
48    /// Create a new 'DownloadManager' and specify the maximum number of concurrent downloads
49    /// The default timeout is set to 60 seconds
50    ///
51    /// # Arguments
52    /// * `max_concurrent` - Maximum number of concurrent downloads
53    pub fn new(max_concurrent: usize) -> Self {
54        let client = Client::builder()
55            .timeout(Duration::from_secs(60))
56            .pool_max_idle_per_host(20)
57            .build()
58            .unwrap_or_default();
59
60        Self { client, max_concurrent }
61    }
62
63    /// Create a 'DownloadManager' with a custom 'Client'
64    pub fn with_client(client: Client, max_concurrent: usize) -> Self {
65        Self { client, max_concurrent }
66    }
67
68    /// Download the photos from the given URL and save to the specified output directory
69    /// Resumable upload is supported
70    ///
71    /// # Arguments
72    /// * `photo` - Photos to download
73    /// * `output_dir` - Output directory
74    /// * `quality` - Download quality
75    ///
76    /// # Returns
77    /// The path to download the file
78    pub async fn download_photo<P: AsRef<Path>>(
79        &self,
80        photo: &Photo,
81        output_dir: P,
82        quality: ImageQuality,
83    ) -> Result<PathBuf> {
84        let url = self.get_photo_url(photo, quality);
85        let file_name = format!("photo_{}.jpg", photo.id);
86        self.download_file(&url, output_dir, &file_name).await
87    }
88
89    /// Download the video from the given URL and save to the specified output directory
90    /// Resumable upload is supported
91    ///
92    /// # Arguments
93    /// * `video` - Video to download
94    /// * `output_dir` - Output directory
95    /// * `quality` - Download quality
96    ///
97    /// # Returns
98    /// The path to download the file
99    pub async fn download_video<P: AsRef<Path>>(
100        &self,
101        video: &Video,
102        output_dir: P,
103        quality: VideoQuality,
104    ) -> Result<PathBuf> {
105        let url = self.get_video_url(video, quality);
106        let file_name = format!("video_{}.mp4", video.id);
107        self.download_file(&url, output_dir, &file_name).await
108    }
109
110    /// Download photos in batches
111    ///
112    /// # Arguments
113    /// * `photos` - A list of photos to download
114    /// * `output_dir` - Output directory
115    /// * `quality` - Download quality
116    /// * `progress_callback` - Optional progress callback function
117    ///
118    /// # Returns
119    /// A list of files that have been successfully downloaded
120    pub async fn batch_download_photos<P: AsRef<Path>>(
121        &self,
122        photos: &[Photo],
123        output_dir: P,
124        quality: ImageQuality,
125        progress_callback: Option<ProgressCallback>,
126    ) -> Result<Vec<PathBuf>> {
127        let output_dir = output_dir.as_ref().to_path_buf();
128        let semaphore = Arc::new(Semaphore::new(self.max_concurrent));
129
130        let mut handles = Vec::with_capacity(photos.len());
131
132        for photo in photos {
133            let permit = Arc::clone(&semaphore).acquire_owned();
134            let photo = photo.clone();
135            let dir = output_dir.clone();
136            let client = self.client.clone();
137            let callback = progress_callback;
138
139            let handle = tokio::spawn(async move {
140                let _permit = permit.await.map_err(|_| PexelsError::AsyncError)?;
141
142                let url = match quality {
143                    ImageQuality::Original => &photo.src.original,
144                    ImageQuality::Large2x => &photo.src.large2x,
145                    ImageQuality::Large => &photo.src.large,
146                    ImageQuality::Medium => &photo.src.medium,
147                    ImageQuality::Small => &photo.src.small,
148                    ImageQuality::Portrait => &photo.src.portrait,
149                    ImageQuality::Landscape => &photo.src.landscape,
150                    ImageQuality::Tiny => &photo.src.tiny,
151                };
152
153                let file_name = format!("photo_{}.jpg", photo.id);
154                let path = dir.join(&file_name);
155
156                // Make sure the directory exists
157                if !dir.exists() {
158                    fs::create_dir_all(&dir).await?;
159                }
160
161                // Resumable upload logic
162                let mut headers = HeaderMap::new();
163                let mut range_start = 0;
164
165                if path.exists() {
166                    if let Ok(metadata) = fs::metadata(&path).await {
167                        range_start = metadata.len();
168                        headers.insert(
169                            header::RANGE,
170                            format!("bytes={range_start}-").parse().unwrap(),
171                        );
172                    }
173                }
174
175                // Download the file
176                let response = client.get(url).headers(headers).send().await?;
177
178                if !response.status().is_success() {
179                    return Err(PexelsError::DownloadError(format!(
180                        "Failed to download file: {}",
181                        response.status()
182                    )));
183                }
184
185                // Get the file size
186                let total_size = response.content_length().unwrap_or(0) + range_start;
187
188                let mut file = if range_start > 0 {
189                    fs::OpenOptions::new().append(true).open(&path).await?
190                } else {
191                    fs::File::create(&path).await?
192                };
193
194                let mut stream = response.bytes_stream();
195                let mut downloaded = range_start;
196
197                while let Some(chunk) = stream.next().await {
198                    let chunk = chunk?;
199                    file.write_all(&chunk).await?;
200
201                    downloaded += chunk.len() as u64;
202
203                    // Call progress callback (if provided)
204                    if let Some(cb) = callback {
205                        cb(downloaded, total_size);
206                    }
207                }
208
209                Ok::<PathBuf, PexelsError>(path)
210            });
211
212            handles.push(handle);
213        }
214
215        // wait for all downloads to complete
216        let results = futures::future::join_all(handles).await;
217
218        // Process the results
219        let mut successful_downloads = Vec::new();
220        for result in results {
221            match result {
222                Ok(Ok(path)) => successful_downloads.push(path),
223                Ok(Err(e)) => eprintln!("Download error: {e}"),
224                Err(e) => eprintln!("Task join error: {e}"),
225            }
226        }
227
228        Ok(successful_downloads)
229    }
230
231    /// Download videos in batches
232    ///
233    /// # Arguments
234    /// * `videos` - A list of videos to download
235    /// * `output_dir` - Output directory
236    /// * `quality` - Download quality
237    /// * `progress_callback` - Optional progress callback function
238    ///
239    /// # Returns
240    /// A list of files that have been successfully downloaded
241    pub async fn batch_download_videos<P: AsRef<Path>>(
242        &self,
243        videos: &[Video],
244        output_dir: P,
245        quality: VideoQuality,
246        progress_callback: Option<ProgressCallback>,
247    ) -> Result<Vec<PathBuf>> {
248        let output_dir = output_dir.as_ref().to_path_buf();
249        let semaphore = Arc::new(Semaphore::new(self.max_concurrent));
250
251        let mut handles = Vec::with_capacity(videos.len());
252
253        for video in videos {
254            let permit = Arc::clone(&semaphore).acquire_owned();
255            let video = video.clone();
256            let dir = output_dir.clone();
257            let client = self.client.clone();
258            let callback = progress_callback;
259
260            let handle = tokio::spawn(async move {
261                let _permit = permit.await.map_err(|_| PexelsError::AsyncError)?;
262
263                // 获取对应质量的视频 URL
264                let video_file = video
265                    .video_files
266                    .iter()
267                    .find(|file| match quality {
268                        VideoQuality::HD => file.quality == "hd" || file.quality == "HD",
269                        VideoQuality::SD => file.quality == "sd",
270                        VideoQuality::Tiny => {
271                            file.file_type == "video/mp4"
272                                && (file.width.unwrap_or(0) <= 640
273                                    || file.height.unwrap_or(0) <= 360)
274                        }
275                    })
276                    .ok_or_else(|| {
277                        PexelsError::DownloadError("No suitable video file found".to_string())
278                    })?;
279
280                let url = &video_file.link;
281                let file_name = format!("video_{}.mp4", video.id);
282                let path = dir.join(&file_name);
283
284                // Make sure the directory exists
285                if !dir.exists() {
286                    fs::create_dir_all(&dir).await?;
287                }
288
289                // Resumable upload logic
290                let mut headers = HeaderMap::new();
291                let mut range_start = 0;
292
293                if path.exists() {
294                    if let Ok(metadata) = fs::metadata(&path).await {
295                        range_start = metadata.len();
296                        headers.insert(
297                            header::RANGE,
298                            format!("bytes={range_start}-").parse().unwrap(),
299                        );
300                    }
301                }
302
303                // Download the file
304                let response = client.get(url).headers(headers).send().await?;
305
306                if !response.status().is_success() {
307                    return Err(PexelsError::DownloadError(format!(
308                        "Failed to download file: {}",
309                        response.status()
310                    )));
311                }
312
313                // Get the file size
314                let total_size = response.content_length().unwrap_or(0) + range_start;
315
316                let mut file = if range_start > 0 {
317                    fs::OpenOptions::new().append(true).open(&path).await?
318                } else {
319                    fs::File::create(&path).await?
320                };
321
322                let mut stream = response.bytes_stream();
323                let mut downloaded = range_start;
324
325                while let Some(chunk) = stream.next().await {
326                    let chunk = chunk?;
327                    file.write_all(&chunk).await?;
328
329                    downloaded += chunk.len() as u64;
330
331                    // Call progress callback (if provided)
332                    if let Some(cb) = callback {
333                        cb(downloaded, total_size);
334                    }
335                }
336
337                Ok::<PathBuf, PexelsError>(path)
338            });
339
340            handles.push(handle);
341        }
342
343        // Wait for all downloads to complete
344        let results = futures::future::join_all(handles).await;
345
346        // Process the results
347        let mut successful_downloads = Vec::new();
348        for result in results {
349            match result {
350                Ok(Ok(path)) => successful_downloads.push(path),
351                Ok(Err(e)) => eprintln!("Download error: {e}"),
352                Err(e) => eprintln!("Task join error: {e}"),
353            }
354        }
355
356        Ok(successful_downloads)
357    }
358
359    /// Download a single file
360    ///
361    /// # Arguments
362    /// * `url` - File URL
363    /// * `output_dir` - Output directory
364    /// * `file_name` - Filename
365    ///
366    /// # Returns
367    /// The path to download the file
368    async fn download_file<P: AsRef<Path>>(
369        &self,
370        url: &str,
371        output_dir: P,
372        file_name: &str,
373    ) -> Result<PathBuf> {
374        let output_dir = output_dir.as_ref().to_path_buf();
375        let path = output_dir.join(file_name);
376
377        // Make sure the directory exists
378        if !output_dir.exists() {
379            fs::create_dir_all(&output_dir).await?;
380        }
381
382        // Resumable upload logic
383        let mut headers = HeaderMap::new();
384        let mut range_start = 0;
385
386        if path.exists() {
387            if let Ok(metadata) = fs::metadata(&path).await {
388                range_start = metadata.len();
389                headers.insert(header::RANGE, format!("bytes={range_start}-").parse().unwrap());
390            }
391        }
392
393        // Send a request
394        let response = self.client.get(url).headers(headers).send().await?;
395
396        if !response.status().is_success() {
397            return Err(PexelsError::DownloadError(format!(
398                "Failed to download file: {}",
399                response.status()
400            )));
401        }
402
403        // Get the file size
404        let _total_size = response.content_length().unwrap_or(0) + range_start;
405
406        let mut file = if range_start > 0 {
407            fs::OpenOptions::new().append(true).open(&path).await?
408        } else {
409            fs::File::create(&path).await?
410        };
411
412        let mut stream = response.bytes_stream();
413
414        while let Some(chunk) = stream.next().await {
415            let chunk = chunk?;
416            file.write_all(&chunk).await?;
417        }
418
419        Ok(path)
420    }
421
422    /// Get the photo URL
423    fn get_photo_url(&self, photo: &Photo, quality: ImageQuality) -> String {
424        match quality {
425            ImageQuality::Original => photo.src.original.clone(),
426            ImageQuality::Large2x => photo.src.large2x.clone(),
427            ImageQuality::Large => photo.src.large.clone(),
428            ImageQuality::Medium => photo.src.medium.clone(),
429            ImageQuality::Small => photo.src.small.clone(),
430            ImageQuality::Portrait => photo.src.portrait.clone(),
431            ImageQuality::Landscape => photo.src.landscape.clone(),
432            ImageQuality::Tiny => photo.src.tiny.clone(),
433        }
434    }
435
436    /// Get the video URL
437    fn get_video_url(&self, video: &Video, quality: VideoQuality) -> String {
438        let video_file = video
439            .video_files
440            .iter()
441            .find(|file| match quality {
442                VideoQuality::HD => file.quality == "hd" || file.quality == "HD",
443                VideoQuality::SD => file.quality == "sd",
444                VideoQuality::Tiny => {
445                    file.file_type == "video/mp4"
446                        && (file.width.unwrap_or(0) <= 640 || file.height.unwrap_or(0) <= 360)
447                }
448            })
449            .unwrap_or_else(|| {
450                // If you can't find the specified quality, return the first video file
451                video.video_files.first().unwrap_or_else(|| {
452                    panic!("No video files available for video ID: {}", video.id)
453                })
454            });
455
456        video_file.link.clone()
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463    use crate::models::PhotoSources;
464    use tokio::test;
465
466    // Simulate the Photo data structure
467    fn mock_photo() -> Photo {
468        Photo {
469            id: 1,
470            width: 800,
471            height: 600,
472            url: "https://www.pexels.com/photo/1".to_string(),
473            photographer: "Test Photographer".to_string(),
474            photographer_url: Some("https://www.pexels.com/photographer".to_string()),
475            photographer_id: Some(1),
476            avg_color: Some("#FFFFFF".to_string()),
477            src: PhotoSources {
478                original: "https://images.pexels.com/photos/1/original.jpg".to_string(),
479                large2x: "https://images.pexels.com/photos/1/large2x.jpg".to_string(),
480                large: "https://images.pexels.com/photos/1/large.jpg".to_string(),
481                medium: "https://images.pexels.com/photos/1/medium.jpg".to_string(),
482                small: "https://images.pexels.com/photos/1/small.jpg".to_string(),
483                portrait: "https://images.pexels.com/photos/1/portrait.jpg".to_string(),
484                landscape: "https://images.pexels.com/photos/1/landscape.jpg".to_string(),
485                tiny: "https://images.pexels.com/photos/1/tiny.jpg".to_string(),
486            },
487            alt: Some("Test Photo".to_string()),
488        }
489    }
490
491    #[test]
492    async fn test_get_photo_url() {
493        let manager = DownloadManager::new(5);
494        let photo = mock_photo();
495
496        assert_eq!(
497            manager.get_photo_url(&photo, ImageQuality::Original),
498            "https://images.pexels.com/photos/1/original.jpg"
499        );
500        assert_eq!(
501            manager.get_photo_url(&photo, ImageQuality::Large2x),
502            "https://images.pexels.com/photos/1/large2x.jpg"
503        );
504    }
505}