use anyhow::{anyhow, Result};
use clap::{Args, Subcommand};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Semaphore;
use crate::client::RommClient;
use crate::core::download::download_directory;
use crate::core::utils;
use crate::endpoints::roms::GetRoms;
use crate::services::RomService;
const DEFAULT_CONCURRENCY: usize = 4;
#[derive(Args, Debug)]
pub struct DownloadCommand {
#[command(subcommand)]
pub action: Option<DownloadAction>,
pub rom_id: Option<u64>,
#[arg(short, long, global = true)]
pub output: Option<PathBuf>,
#[arg(long, global = true)]
pub batch: bool,
#[arg(long, global = true)]
pub platform_id: Option<u64>,
#[arg(long, global = true)]
pub search_term: Option<String>,
#[arg(long, default_value_t = DEFAULT_CONCURRENCY, global = true)]
pub jobs: usize,
#[arg(long, default_value_t = true, global = true)]
pub resume: bool,
}
#[derive(Subcommand, Debug)]
pub enum DownloadAction {
#[command(visible_alias = "one")]
Id {
id: u64,
},
#[command(visible_alias = "all")]
Batch,
}
fn make_progress_style() -> ProgressStyle {
ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40.cyan/blue} {bytes}/{total_bytes} ({eta}) {msg}",
)
.unwrap()
.progress_chars("#>-")
}
async fn download_one(
client: &RommClient,
rom_id: u64,
name: &str,
save_path: &std::path::Path,
pb: ProgressBar,
) -> Result<()> {
pb.set_message(name.to_string());
client
.download_rom(rom_id, save_path, {
let pb = pb.clone();
move |received, total| {
if pb.length() != Some(total) {
pb.set_length(total);
}
pb.set_position(received);
}
})
.await?;
pb.finish_with_message(format!("✓ {name}"));
Ok(())
}
pub async fn handle(cmd: DownloadCommand, client: &RommClient) -> Result<()> {
let output_dir = cmd.output.unwrap_or_else(download_directory);
tokio::fs::create_dir_all(&output_dir)
.await
.map_err(|e| anyhow!("create download dir {:?}: {e}", output_dir))?;
let is_batch = matches!(cmd.action, Some(DownloadAction::Batch)) || cmd.batch;
if is_batch {
if cmd.platform_id.is_none() && cmd.search_term.is_none() {
return Err(anyhow!(
"Batch download requires at least --platform-id or --search-term to scope the download"
));
}
let ep = GetRoms {
search_term: cmd.search_term.clone(),
platform_id: cmd.platform_id,
collection_id: None,
smart_collection_id: None,
virtual_collection_id: None,
limit: Some(9999),
offset: None,
};
let service = RomService::new(client);
let results = service.search_roms(&ep).await?;
if results.items.is_empty() {
println!("No ROMs found matching the given filters.");
return Ok(());
}
println!(
"Found {} ROM(s). Starting download with {} concurrent connections...",
results.items.len(),
cmd.jobs
);
let mp = MultiProgress::new();
let semaphore = Arc::new(Semaphore::new(cmd.jobs));
let mut handles = Vec::new();
for rom in results.items {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let client = client.clone();
let dir = output_dir.clone();
let pb = mp.add(ProgressBar::new(0));
pb.set_style(make_progress_style());
let name = rom.name.clone();
let rom_id = rom.id;
let base = utils::sanitize_filename(&rom.fs_name);
let stem = base.rsplit_once('.').map(|(s, _)| s).unwrap_or(&base);
let save_path = dir.join(format!("{stem}.zip"));
handles.push(tokio::spawn(async move {
let result = download_one(&client, rom_id, &name, &save_path, pb).await;
drop(permit);
if let Err(e) = &result {
eprintln!("error downloading {name} (id={rom_id}): {e}");
}
result
}));
}
let mut successes = 0u32;
let mut failures = 0u32;
for handle in handles {
match handle.await {
Ok(Ok(())) => successes += 1,
_ => failures += 1,
}
}
println!("\nBatch complete: {successes} succeeded, {failures} failed.");
} else {
let rom_id = if let Some(DownloadAction::Id { id }) = cmd.action {
id
} else {
cmd.rom_id
.ok_or_else(|| anyhow!("ROM ID is required (e.g. 'download 123' or 'download batch --search-term ...')"))?
};
let save_path = output_dir.join(format!("rom_{rom_id}.zip"));
let mp = MultiProgress::new();
let pb = mp.add(ProgressBar::new(0));
pb.set_style(make_progress_style());
download_one(client, rom_id, &format!("ROM {rom_id}"), &save_path, pb).await?;
println!("Saved to {:?}", save_path);
}
Ok(())
}