romm-api 1.0.0

HTTP client and domain logic for the RomM API
Documentation
use std::sync::Arc;

use crate::client::RommClient;
use crate::core::extras::DownloadTarget;
use crate::error::DownloadError;
use crate::types::Rom;

use super::super::extras_job::{finalize_extras_job_status, ExtrasItemResult, ExtrasJob};
use super::super::paths::resolve_download_directory;
use super::super::transfer::{download_target_with_fallback, prepare_download_target_destination};
use super::DownloadManager;

impl DownloadManager {
    pub fn start_extras_download(
        &self,
        rom: &Rom,
        selected: Vec<DownloadTarget>,
        client: RommClient,
        configured_download_dir: Option<&str>,
    ) -> Result<(), DownloadError> {
        if selected.is_empty() {
            return Err(DownloadError::NoExtrasTargets);
        }

        let _ = resolve_download_directory(configured_download_dir)?;

        let platform = rom
            .platform_display_name
            .as_deref()
            .or(rom.platform_custom_name.as_deref())
            .unwrap_or("")
            .to_string();

        let total_items = selected.len();
        let job = ExtrasJob::new(rom.id, rom.name.clone(), platform, total_items);
        let job_id = job.id;

        match self.extras_jobs.lock() {
            Ok(mut jobs) => jobs.push(job),
            Err(err) => {
                eprintln!("warning: extras job list lock poisoned: {}", err);
                return Err(DownloadError::ExtrasJobListPoisoned(err.to_string()));
            }
        }

        let extras_jobs = self.extras_jobs.clone();
        tokio::spawn(async move {
            let semaphore = Arc::new(tokio::sync::Semaphore::new(4));
            let mut handles = Vec::new();

            for target in selected {
                let permit = match semaphore.clone().acquire_owned().await {
                    Ok(p) => p,
                    Err(_) => break,
                };
                let client = client.clone();
                let extras_jobs = extras_jobs.clone();
                handles.push(tokio::spawn(async move {
                    let mut on_progress = |_r: u64, _t: u64| {};
                    let download_result = match prepare_download_target_destination(&target).await {
                        Ok(true) => Ok(()),
                        Ok(false) => {
                            download_target_with_fallback(
                                &client,
                                &target,
                                |_, _| false,
                                &mut on_progress,
                            )
                            .await
                        }
                        Err(err) => Err(err),
                    };

                    drop(permit);

                    let (ok, err) = match download_result {
                        Ok(()) => (true, None),
                        Err(e) => (false, Some(e.to_string())),
                    };

                    let item = ExtrasItemResult {
                        title: target.title.clone(),
                        kind: target.kind,
                        ok,
                        error: err,
                    };

                    if let Ok(mut list) = extras_jobs.lock() {
                        if let Some(j) = list.iter_mut().find(|j| j.id == job_id) {
                            j.completed_items = j.completed_items.saturating_add(1);
                            j.item_results.push(item);
                            if j.completed_items >= j.total_items {
                                j.status = finalize_extras_job_status(&j.item_results);
                            }
                        }
                    }
                }));
            }

            for h in handles {
                let _ = h.await;
            }
        });

        Ok(())
    }
}