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 smart_collection_id: None,
122 virtual_collection_id: None,
123 limit: Some(9999),
124 offset: None,
125 };
126
127 let service = RomService::new(client);
128 let results = service.search_roms(&ep).await?;
129
130 if results.items.is_empty() {
131 println!("No ROMs found matching the given filters.");
132 return Ok(());
133 }
134
135 println!(
136 "Found {} ROM(s). Starting download with {} concurrent connections...",
137 results.items.len(),
138 cmd.jobs
139 );
140
141 let mp = MultiProgress::new();
142 let semaphore = Arc::new(Semaphore::new(cmd.jobs));
143 let mut handles = Vec::new();
144
145 for rom in results.items {
146 let permit = semaphore.clone().acquire_owned().await.unwrap();
147 let client = client.clone();
148 let dir = output_dir.clone();
149 let pb = mp.add(ProgressBar::new(0));
150 pb.set_style(make_progress_style());
151
152 let name = rom.name.clone();
153 let rom_id = rom.id;
154 let base = utils::sanitize_filename(&rom.fs_name);
155 let stem = base.rsplit_once('.').map(|(s, _)| s).unwrap_or(&base);
156 let save_path = dir.join(format!("{stem}.zip"));
157
158 handles.push(tokio::spawn(async move {
159 let result = download_one(&client, rom_id, &name, &save_path, pb).await;
160 drop(permit);
161 if let Err(e) = &result {
162 eprintln!("error downloading {name} (id={rom_id}): {e}");
163 }
164 result
165 }));
166 }
167
168 let mut successes = 0u32;
169 let mut failures = 0u32;
170 for handle in handles {
171 match handle.await {
172 Ok(Ok(())) => successes += 1,
173 _ => failures += 1,
174 }
175 }
176
177 println!("\nBatch complete: {successes} succeeded, {failures} failed.");
178 } else {
179 let rom_id = if let Some(DownloadAction::Id { id }) = cmd.action {
181 id
182 } else {
183 cmd.rom_id
184 .ok_or_else(|| anyhow!("ROM ID is required (e.g. 'download 123' or 'download batch --search-term ...')"))?
185 };
186
187 let save_path = output_dir.join(format!("rom_{rom_id}.zip"));
188
189 let mp = MultiProgress::new();
190 let pb = mp.add(ProgressBar::new(0));
191 pb.set_style(make_progress_style());
192
193 download_one(client, rom_id, &format!("ROM {rom_id}"), &save_path, pb).await?;
194
195 println!("Saved to {:?}", save_path);
196 }
197
198 Ok(())
199}