romm_cli/commands/
download.rs1use anyhow::{anyhow, Result};
2use clap::{Args, Subcommand};
3use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
4use std::path::PathBuf;
5use std::sync::Arc;
6use tokio::sync::Semaphore;
7
8use crate::client::RommClient;
9use crate::core::download::download_directory;
10use crate::core::utils;
11use crate::endpoints::roms::GetRoms;
12use crate::services::RomService;
13
14const DEFAULT_CONCURRENCY: usize = 4;
16
17#[derive(Args, Debug)]
19pub struct DownloadCommand {
20 #[command(subcommand)]
21 pub action: Option<DownloadAction>,
22
23 pub rom_id: Option<u64>,
25
26 #[arg(short, long, global = true)]
28 pub output: Option<PathBuf>,
29
30 #[arg(long, global = true)]
32 pub batch: bool,
33
34 #[arg(long, global = true)]
36 pub platform_id: Option<u64>,
37
38 #[arg(long, global = true)]
40 pub search_term: Option<String>,
41
42 #[arg(long, default_value_t = DEFAULT_CONCURRENCY, global = true)]
44 pub jobs: usize,
45
46 #[arg(long, default_value_t = true, global = true)]
48 pub resume: bool,
49}
50
51#[derive(Subcommand, Debug)]
52pub enum DownloadAction {
53 #[command(visible_alias = "one")]
55 Id {
56 id: u64,
58 },
59 #[command(visible_alias = "all")]
61 Batch,
62}
63
64fn make_progress_style() -> ProgressStyle {
65 ProgressStyle::with_template(
66 "[{elapsed_precise}] {bar:40.cyan/blue} {bytes}/{total_bytes} ({eta}) {msg}",
67 )
68 .unwrap()
69 .progress_chars("#>-")
70}
71
72async fn download_one(
73 client: &RommClient,
74 rom_id: u64,
75 name: &str,
76 save_path: &std::path::Path,
77 pb: ProgressBar,
78) -> Result<()> {
79 pb.set_message(name.to_string());
80
81 client
82 .download_rom(rom_id, save_path, {
83 let pb = pb.clone();
84 move |received, total| {
85 if pb.length() != Some(total) {
86 pb.set_length(total);
87 }
88 pb.set_position(received);
89 }
90 })
91 .await?;
92
93 pb.finish_with_message(format!("✓ {name}"));
94 Ok(())
95}
96
97pub async fn handle(cmd: DownloadCommand, client: &RommClient) -> Result<()> {
98 let output_dir = cmd.output.unwrap_or_else(download_directory);
99
100 tokio::fs::create_dir_all(&output_dir)
102 .await
103 .map_err(|e| anyhow!("create download dir {:?}: {e}", output_dir))?;
104
105 let is_batch = matches!(cmd.action, Some(DownloadAction::Batch)) || cmd.batch;
108
109 if is_batch {
110 if cmd.platform_id.is_none() && cmd.search_term.is_none() {
112 return Err(anyhow!(
113 "Batch download requires at least --platform-id or --search-term to scope the download"
114 ));
115 }
116
117 let ep = GetRoms {
118 search_term: cmd.search_term.clone(),
119 platform_id: cmd.platform_id,
120 collection_id: None,
121 limit: Some(9999),
122 offset: None,
123 };
124
125 let service = RomService::new(client);
126 let results = service.search_roms(&ep).await?;
127
128 if results.items.is_empty() {
129 println!("No ROMs found matching the given filters.");
130 return Ok(());
131 }
132
133 println!(
134 "Found {} ROM(s). Starting download with {} concurrent connections...",
135 results.items.len(),
136 cmd.jobs
137 );
138
139 let mp = MultiProgress::new();
140 let semaphore = Arc::new(Semaphore::new(cmd.jobs));
141 let mut handles = Vec::new();
142
143 for rom in results.items {
144 let permit = semaphore.clone().acquire_owned().await.unwrap();
145 let client = client.clone();
146 let dir = output_dir.clone();
147 let pb = mp.add(ProgressBar::new(0));
148 pb.set_style(make_progress_style());
149
150 let name = rom.name.clone();
151 let rom_id = rom.id;
152 let base = utils::sanitize_filename(&rom.fs_name);
153 let stem = base.rsplit_once('.').map(|(s, _)| s).unwrap_or(&base);
154 let save_path = dir.join(format!("{stem}.zip"));
155
156 handles.push(tokio::spawn(async move {
157 let result = download_one(&client, rom_id, &name, &save_path, pb).await;
158 drop(permit);
159 if let Err(e) = &result {
160 eprintln!("error downloading {name} (id={rom_id}): {e}");
161 }
162 result
163 }));
164 }
165
166 let mut successes = 0u32;
167 let mut failures = 0u32;
168 for handle in handles {
169 match handle.await {
170 Ok(Ok(())) => successes += 1,
171 _ => failures += 1,
172 }
173 }
174
175 println!("\nBatch complete: {successes} succeeded, {failures} failed.");
176 } else {
177 let rom_id = if let Some(DownloadAction::Id { id }) = cmd.action {
179 id
180 } else {
181 cmd.rom_id
182 .ok_or_else(|| anyhow!("ROM ID is required (e.g. 'download 123' or 'download batch --search-term ...')"))?
183 };
184
185 let save_path = output_dir.join(format!("rom_{rom_id}.zip"));
186
187 let mp = MultiProgress::new();
188 let pb = mp.add(ProgressBar::new(0));
189 pb.set_style(make_progress_style());
190
191 download_one(client, rom_id, &format!("ROM {rom_id}"), &save_path, pb).await?;
192
193 println!("Saved to {:?}", save_path);
194 }
195
196 Ok(())
197}