romm_api/core/
library_scan.rs1use std::time::{Duration, Instant};
4
5use anyhow::{anyhow, Context, Result};
6use serde_json::Value;
7
8use crate::client::RommClient;
9use crate::core::cache::{RomCache, RomCacheKey};
10use crate::core::interrupt::{cancelled_error, InterruptContext};
11
12pub const SCAN_LIBRARY_TASK_NAME: &str = "scan_library";
13
14#[derive(Clone, Debug, Default)]
16pub enum ScanCacheInvalidate {
17 #[default]
18 None,
19 Platform(u64),
21 AllPlatforms,
23}
24
25#[derive(Clone, Debug)]
27pub struct ScanLibraryOptions {
28 pub wait: bool,
29 pub wait_timeout: Duration,
30 pub cache_invalidate: ScanCacheInvalidate,
31 pub task_kwargs: Option<Value>,
33}
34
35pub fn apply_disk_cache_invalidate(inv: &ScanCacheInvalidate) {
37 match inv {
38 ScanCacheInvalidate::None => {}
39 ScanCacheInvalidate::Platform(pid) => {
40 let mut c = RomCache::load();
41 c.remove(&RomCacheKey::Platform(*pid));
42 }
43 ScanCacheInvalidate::AllPlatforms => {
44 let mut c = RomCache::load();
45 c.remove_all_platform_entries();
46 }
47 }
48}
49
50#[derive(Debug)]
51pub struct ScanLibraryStart {
52 pub task_id: String,
53 pub initial_status: String,
54 pub raw: Value,
55}
56
57pub async fn start_scan_library(
59 client: &RommClient,
60 kwargs: Option<serde_json::Value>,
61) -> Result<ScanLibraryStart> {
62 let raw = client
63 .run_task(SCAN_LIBRARY_TASK_NAME, kwargs)
64 .await
65 .context("failed to start scan_library task")?;
66 let task_id = raw
67 .get("task_id")
68 .and_then(|v| v.as_str())
69 .filter(|s| !s.is_empty())
70 .ok_or_else(|| {
71 anyhow!(
72 "scan response missing task_id (unexpected server response): {}",
73 raw
74 )
75 })?
76 .to_string();
77 let initial_status = raw
78 .get("status")
79 .and_then(|v| v.as_str())
80 .unwrap_or("unknown")
81 .to_string();
82 Ok(ScanLibraryStart {
83 task_id,
84 initial_status,
85 raw,
86 })
87}
88
89fn status_from_json(v: &Value) -> Option<&str> {
90 v.get("status").and_then(|s| s.as_str())
91}
92
93fn is_terminal_status(status: &str) -> bool {
94 status.eq_ignore_ascii_case("finished")
95 || status.eq_ignore_ascii_case("failed")
96 || status.eq_ignore_ascii_case("stopped")
97 || status.eq_ignore_ascii_case("canceled")
98 || status.eq_ignore_ascii_case("cancelled")
99}
100
101fn is_success_status(status: &str) -> bool {
102 status.eq_ignore_ascii_case("finished")
103}
104
105pub async fn wait_for_task_terminal(
109 client: &RommClient,
110 task_id: &str,
111 timeout: Duration,
112 interrupt: Option<&InterruptContext>,
113 mut on_status: impl FnMut(&str),
114) -> Result<Value> {
115 let deadline = Instant::now() + timeout;
116 loop {
117 if Instant::now() >= deadline {
118 anyhow::bail!(
119 "timed out waiting for library scan task {} after {:?}",
120 task_id,
121 timeout
122 );
123 }
124
125 let body = client
126 .get_task_status(task_id)
127 .await
128 .with_context(|| format!("failed to poll task {task_id}"))?;
129 let st = status_from_json(&body).unwrap_or("");
130
131 if is_terminal_status(st) {
132 if is_success_status(st) {
133 return Ok(body);
134 }
135 anyhow::bail!("library scan task ended with status {st:?}: {body}");
136 }
137
138 on_status(st);
139 if let Some(ctx) = interrupt {
140 tokio::select! {
141 _ = tokio::time::sleep(Duration::from_secs(2)) => {},
142 _ = ctx.cancelled() => return Err(cancelled_error().into()),
143 }
144 } else {
145 tokio::time::sleep(Duration::from_secs(2)).await;
146 }
147 }
148}