use std::time::{Duration, Instant};
use anyhow::{anyhow, Context, Result};
use serde_json::Value;
use crate::client::RommClient;
use crate::core::cache::{RomCache, RomCacheKey};
use crate::core::interrupt::{cancelled_error, InterruptContext};
pub const SCAN_LIBRARY_TASK_NAME: &str = "scan_library";
#[derive(Clone, Debug, Default)]
pub enum ScanCacheInvalidate {
#[default]
None,
Platform(u64),
AllPlatforms,
}
#[derive(Clone, Debug)]
pub struct ScanLibraryOptions {
pub wait: bool,
pub wait_timeout: Duration,
pub cache_invalidate: ScanCacheInvalidate,
pub task_kwargs: Option<Value>,
}
pub fn apply_disk_cache_invalidate(inv: &ScanCacheInvalidate) {
match inv {
ScanCacheInvalidate::None => {}
ScanCacheInvalidate::Platform(pid) => {
let mut c = RomCache::load();
c.remove(&RomCacheKey::Platform(*pid));
}
ScanCacheInvalidate::AllPlatforms => {
let mut c = RomCache::load();
c.remove_all_platform_entries();
}
}
}
#[derive(Debug)]
pub struct ScanLibraryStart {
pub task_id: String,
pub initial_status: String,
pub raw: Value,
}
pub async fn start_scan_library(
client: &RommClient,
kwargs: Option<serde_json::Value>,
) -> Result<ScanLibraryStart> {
let raw = client
.run_task(SCAN_LIBRARY_TASK_NAME, kwargs)
.await
.context("failed to start scan_library task")?;
let task_id = raw
.get("task_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
anyhow!(
"scan response missing task_id (unexpected server response): {}",
raw
)
})?
.to_string();
let initial_status = raw
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
Ok(ScanLibraryStart {
task_id,
initial_status,
raw,
})
}
fn status_from_json(v: &Value) -> Option<&str> {
v.get("status").and_then(|s| s.as_str())
}
fn is_terminal_status(status: &str) -> bool {
status.eq_ignore_ascii_case("finished")
|| status.eq_ignore_ascii_case("failed")
|| status.eq_ignore_ascii_case("stopped")
|| status.eq_ignore_ascii_case("canceled")
|| status.eq_ignore_ascii_case("cancelled")
}
fn is_success_status(status: &str) -> bool {
status.eq_ignore_ascii_case("finished")
}
pub async fn wait_for_task_terminal(
client: &RommClient,
task_id: &str,
timeout: Duration,
interrupt: Option<&InterruptContext>,
mut on_status: impl FnMut(&str),
) -> Result<Value> {
let deadline = Instant::now() + timeout;
loop {
if Instant::now() >= deadline {
anyhow::bail!(
"timed out waiting for library scan task {} after {:?}",
task_id,
timeout
);
}
let body = client
.get_task_status(task_id)
.await
.with_context(|| format!("failed to poll task {task_id}"))?;
let st = status_from_json(&body).unwrap_or("");
if is_terminal_status(st) {
if is_success_status(st) {
return Ok(body);
}
anyhow::bail!("library scan task ended with status {st:?}: {body}");
}
on_status(st);
if let Some(ctx) = interrupt {
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(2)) => {},
_ = ctx.cancelled() => return Err(cancelled_error().into()),
}
} else {
tokio::time::sleep(Duration::from_secs(2)).await;
}
}
}