romm_cli/commands/
library_scan.rs1use std::time::{Duration, Instant};
4
5use anyhow::{anyhow, Context, Result};
6use indicatif::ProgressBar;
7use serde_json::Value;
8
9use crate::client::RommClient;
10use crate::core::cache::{RomCache, RomCacheKey};
11
12use super::OutputFormat;
13
14pub const SCAN_LIBRARY_TASK_NAME: &str = "scan_library";
15
16#[derive(Clone, Debug, Default)]
18pub enum ScanCacheInvalidate {
19 #[default]
20 None,
21 Platform(u64),
23 AllPlatforms,
25}
26
27#[derive(Clone, Debug)]
29pub struct ScanLibraryOptions {
30 pub wait: bool,
31 pub wait_timeout: Duration,
32 pub cache_invalidate: ScanCacheInvalidate,
33}
34
35fn apply_cache_invalidate(inv: &ScanCacheInvalidate) {
36 match inv {
37 ScanCacheInvalidate::None => {}
38 ScanCacheInvalidate::Platform(pid) => {
39 let mut c = RomCache::load();
40 c.remove(&RomCacheKey::Platform(*pid));
41 }
42 ScanCacheInvalidate::AllPlatforms => {
43 let mut c = RomCache::load();
44 c.remove_all_platform_entries();
45 }
46 }
47}
48
49#[derive(Debug)]
50pub struct ScanLibraryStart {
51 pub task_id: String,
52 pub initial_status: String,
53 pub raw: Value,
54}
55
56pub async fn start_scan_library(client: &RommClient) -> Result<ScanLibraryStart> {
58 let raw = client
59 .run_task(SCAN_LIBRARY_TASK_NAME, None)
60 .await
61 .context("failed to start scan_library task")?;
62 let task_id = raw
63 .get("task_id")
64 .and_then(|v| v.as_str())
65 .filter(|s| !s.is_empty())
66 .ok_or_else(|| {
67 anyhow!(
68 "scan response missing task_id (unexpected server response): {}",
69 raw
70 )
71 })?
72 .to_string();
73 let initial_status = raw
74 .get("status")
75 .and_then(|v| v.as_str())
76 .unwrap_or("unknown")
77 .to_string();
78 Ok(ScanLibraryStart {
79 task_id,
80 initial_status,
81 raw,
82 })
83}
84
85fn status_from_json(v: &Value) -> Option<&str> {
86 v.get("status").and_then(|s| s.as_str())
87}
88
89fn is_terminal_status(status: &str) -> bool {
90 status.eq_ignore_ascii_case("finished")
91 || status.eq_ignore_ascii_case("failed")
92 || status.eq_ignore_ascii_case("stopped")
93 || status.eq_ignore_ascii_case("canceled")
94 || status.eq_ignore_ascii_case("cancelled")
95}
96
97fn is_success_status(status: &str) -> bool {
98 status.eq_ignore_ascii_case("finished")
99}
100
101pub async fn wait_for_task_terminal(
105 client: &RommClient,
106 task_id: &str,
107 timeout: Duration,
108 mut on_status: impl FnMut(&str),
109) -> Result<Value> {
110 let deadline = Instant::now() + timeout;
111 loop {
112 if Instant::now() >= deadline {
113 anyhow::bail!(
114 "timed out waiting for library scan task {} after {:?}",
115 task_id,
116 timeout
117 );
118 }
119
120 let body = client
121 .get_task_status(task_id)
122 .await
123 .with_context(|| format!("failed to poll task {task_id}"))?;
124 let st = status_from_json(&body).unwrap_or("");
125
126 if is_terminal_status(st) {
127 if is_success_status(st) {
128 return Ok(body);
129 }
130 anyhow::bail!("library scan task ended with status {st:?}: {body}");
131 }
132
133 on_status(st);
134 tokio::time::sleep(Duration::from_secs(2)).await;
135 }
136}
137
138pub async fn wait_for_scan_task(
140 client: &RommClient,
141 task_id: &str,
142 timeout: Duration,
143) -> Result<Value> {
144 let pb = ProgressBar::new_spinner();
145 pb.enable_steady_tick(Duration::from_millis(120));
146 pb.set_message(format!("Waiting for library scan (task {task_id})…"));
147
148 let result = wait_for_task_terminal(client, task_id, timeout, |st| {
149 pb.set_message(format!("Library scan: {st}…"));
150 })
151 .await;
152
153 pb.finish_and_clear();
154 result
155}
156
157pub async fn run_scan_library_flow(
159 client: &RommClient,
160 options: ScanLibraryOptions,
161 format: OutputFormat,
162) -> Result<()> {
163 match format {
164 OutputFormat::Text => println!("Triggering library scan..."),
165 OutputFormat::Json => {}
166 }
167
168 let start = start_scan_library(client).await?;
169
170 match format {
171 OutputFormat::Text => println!(
172 "Scan started: task_id={}, status={}",
173 start.task_id, start.initial_status
174 ),
175 OutputFormat::Json if !options.wait => {
176 println!("{}", serde_json::to_string_pretty(&start.raw)?);
177 }
178 OutputFormat::Json => {}
179 }
180
181 if options.wait {
182 let final_body = wait_for_scan_task(client, &start.task_id, options.wait_timeout).await?;
183 apply_cache_invalidate(&options.cache_invalidate);
184 match format {
185 OutputFormat::Text => println!("Library scan finished successfully."),
186 OutputFormat::Json => {
187 let mut out = start.raw;
188 if let Value::Object(ref mut m) = out {
189 m.insert("final_status".into(), final_body);
190 }
191 println!("{}", serde_json::to_string_pretty(&out)?);
192 }
193 }
194 }
195
196 Ok(())
197}