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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum VideoQuality {
31 HD,
32 SD,
33 Tiny,
34}
35
36pub type ProgressCallback = fn(current: u64, total: u64);
38
39type Result<T> = std::result::Result<T, PexelsError>;
41
42pub struct DownloadManager {
43 client: Client,
44 max_concurrent: usize,
45}
46
47impl DownloadManager {
48 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 pub fn with_client(client: Client, max_concurrent: usize) -> Self {
65 Self { client, max_concurrent }
66 }
67
68 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 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 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 if !dir.exists() {
158 fs::create_dir_all(&dir).await?;
159 }
160
161 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 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 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 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 let results = futures::future::join_all(handles).await;
217
218 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 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 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 if !dir.exists() {
286 fs::create_dir_all(&dir).await?;
287 }
288
289 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 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 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 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 let results = futures::future::join_all(handles).await;
345
346 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 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 if !output_dir.exists() {
379 fs::create_dir_all(&output_dir).await?;
380 }
381
382 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 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 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 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 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 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 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}