Skip to main content

romm_cli/commands/
roms.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use anyhow::Result;
5use clap::{Args, Subcommand};
6use indicatif::{ProgressBar, ProgressStyle};
7
8use crate::client::RommClient;
9use crate::commands::library_scan::{
10    run_scan_library_flow, ScanCacheInvalidate, ScanLibraryOptions,
11};
12use crate::commands::print::print_roms_table;
13use crate::commands::OutputFormat;
14use crate::endpoints::roms::GetRoms;
15use crate::services::RomService;
16
17/// CLI entrypoint for listing/searching ROMs via `/api/roms`.
18#[derive(Args, Debug)]
19pub struct RomsCommand {
20    #[command(subcommand)]
21    pub action: Option<RomsAction>,
22
23    /// Search term to filter roms
24    #[arg(long, global = true, visible_aliases = ["query", "q"])]
25    pub search_term: Option<String>,
26
27    /// Filter by platform ID
28    #[arg(long, global = true, visible_alias = "id")]
29    pub platform_id: Option<u64>,
30
31    /// Page size limit
32    #[arg(long, global = true)]
33    pub limit: Option<u32>,
34
35    /// Page offset
36    #[arg(long, global = true)]
37    pub offset: Option<u32>,
38
39    /// Output as JSON (overrides global --json when set).
40    #[arg(long, global = true)]
41    pub json: bool,
42}
43
44#[derive(Subcommand, Debug)]
45pub enum RomsAction {
46    /// List available ROMs (default)
47    #[command(visible_alias = "ls")]
48    List,
49    /// Get detailed information for a single ROM
50    #[command(visible_alias = "info")]
51    Get {
52        /// The ID of the ROM
53        id: u64,
54    },
55    /// Upload a ROM file to a platform
56    #[command(visible_alias = "up")]
57    Upload {
58        /// The platform ID to upload to
59        platform_id: u64,
60        /// The file to upload
61        file: PathBuf,
62        /// Trigger a library scan after upload completes
63        #[arg(short, long)]
64        scan: bool,
65        /// Wait until the library scan finishes (requires `--scan`; polls every 2 seconds)
66        #[arg(long, requires = "scan")]
67        wait: bool,
68        /// Max seconds to wait when `--wait` is set (default: 3600)
69        #[arg(long, requires = "wait")]
70        wait_timeout_secs: Option<u64>,
71    },
72}
73
74fn make_progress_style() -> ProgressStyle {
75    ProgressStyle::with_template(
76        "[{elapsed_precise}] {bar:40.cyan/blue} {bytes}/{total_bytes} ({eta}) {msg}",
77    )
78    .unwrap()
79    .progress_chars("#>-")
80}
81
82async fn upload_one(
83    client: &RommClient,
84    platform_id: u64,
85    file_path: std::path::PathBuf,
86    pb: ProgressBar,
87) -> Result<()> {
88    let filename = file_path
89        .file_name()
90        .and_then(|n| n.to_str())
91        .unwrap_or("file")
92        .to_string();
93
94    pb.set_message(format!("Uploading {}", filename));
95
96    client
97        .upload_rom(platform_id, &file_path, {
98            let pb = pb.clone();
99            move |uploaded, total| {
100                if pb.length() != Some(total) {
101                    pb.set_length(total);
102                }
103                pb.set_position(uploaded);
104            }
105        })
106        .await?;
107
108    pb.finish_with_message(format!("✓ Upload complete: {}", filename));
109    Ok(())
110}
111
112pub async fn handle(cmd: RomsCommand, client: &RommClient, format: OutputFormat) -> Result<()> {
113    let action = cmd.action.unwrap_or(RomsAction::List);
114
115    match action {
116        RomsAction::List => {
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: cmd.limit,
124                offset: cmd.offset,
125            };
126
127            let service = RomService::new(client);
128            let results = service.search_roms(&ep).await?;
129
130            match format {
131                OutputFormat::Json => {
132                    println!("{}", serde_json::to_string_pretty(&results)?);
133                }
134                OutputFormat::Text => {
135                    print_roms_table(&results);
136                }
137            }
138        }
139        RomsAction::Get { id } => {
140            let service = RomService::new(client);
141            let rom = service.get_rom(id).await?;
142
143            match format {
144                OutputFormat::Json => {
145                    println!("{}", serde_json::to_string_pretty(&rom)?);
146                }
147                OutputFormat::Text => {
148                    // For now, reuse the table printer for a single item
149                    // or just pretty-print the JSON.
150                    println!("{}", serde_json::to_string_pretty(&rom)?);
151                }
152            }
153        }
154        RomsAction::Upload {
155            file,
156            platform_id,
157            scan,
158            wait,
159            wait_timeout_secs,
160        } => {
161            if !file.exists() {
162                anyhow::bail!("File or directory does not exist: {:?}", file);
163            }
164
165            let mut files = Vec::new();
166            if file.is_dir() {
167                let mut entries = tokio::fs::read_dir(&file).await?;
168                while let Some(entry) = entries.next_entry().await? {
169                    let path = entry.path();
170                    if path.is_file() {
171                        files.push(path);
172                    }
173                }
174                files.sort(); // Consistent order
175            } else {
176                files.push(file);
177            }
178
179            if files.is_empty() {
180                println!("No files found to upload.");
181                return Ok(());
182            }
183
184            if files.len() > 1 {
185                println!("Found {} files to upload.", files.len());
186            }
187
188            let mp = indicatif::MultiProgress::new();
189            let mut successes = 0u32;
190            for path in files {
191                let pb = mp.add(ProgressBar::new(0));
192                pb.set_style(make_progress_style());
193                match upload_one(client, platform_id, path.clone(), pb).await {
194                    Ok(()) => successes += 1,
195                    Err(e) => eprintln!("Error uploading {:?}: {}", path, e),
196                }
197            }
198
199            if scan {
200                if successes == 0 {
201                    eprintln!("Skipping library scan: no uploads completed successfully.");
202                } else {
203                    let options = ScanLibraryOptions {
204                        wait,
205                        wait_timeout: Duration::from_secs(wait_timeout_secs.unwrap_or(3600)),
206                        cache_invalidate: if wait {
207                            ScanCacheInvalidate::Platform(platform_id)
208                        } else {
209                            ScanCacheInvalidate::None
210                        },
211                    };
212                    run_scan_library_flow(client, options, format).await?;
213                }
214            }
215        }
216    }
217
218    Ok(())
219}