use chrono::{DateTime, Utc};
use ngdp_cache::generic::GenericCache;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
const WAGO_API_BASE: &str = "https://wago.tools/api";
const WAGO_CACHE_TTL_SECS: u64 = 30 * 60;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WagoBuild {
pub product: String,
pub version: String,
pub created_at: String,
pub build_config: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_config: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cdn_config: Option<String>,
pub is_bgdl: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum WagoBuildsResponse {
Map(std::collections::HashMap<String, Vec<WagoBuild>>),
Array(Vec<WagoBuild>),
}
async fn fetch_builds_uncached() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let url = format!("{WAGO_API_BASE}/builds");
let response = client
.get(&url)
.header("User-Agent", "ngdp-client")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Wago API returned status: {}", response.status()).into());
}
let builds = response.json::<WagoBuildsResponse>().await?;
Ok(builds)
}
pub async fn fetch_builds() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
let cache_enabled = crate::cached_client::is_caching_enabled();
if !cache_enabled {
return fetch_builds_uncached().await;
}
let cache = match GenericCache::with_subdirectory("wago").await {
Ok(cache) => cache,
Err(_) => {
return fetch_builds_uncached().await;
}
};
let cache_key = "builds.json";
let meta_key = "builds.meta";
let cache_path = cache.get_path(cache_key);
let meta_path = cache.get_path(meta_key);
if let Ok(metadata_content) = tokio::fs::read_to_string(&meta_path).await {
if let Ok(timestamp) = metadata_content.trim().parse::<u64>() {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now < timestamp + WAGO_CACHE_TTL_SECS {
if let Ok(cached_data) = tokio::fs::read(&cache_path).await {
if let Ok(builds) = serde_json::from_slice(&cached_data) {
tracing::debug!("Using cached Wago builds data");
return Ok(builds);
}
}
}
}
}
tracing::debug!("Fetching fresh Wago builds data");
let builds = fetch_builds_uncached().await?;
if let Ok(json_data) = serde_json::to_vec(&builds) {
let _ = cache.write(cache_key, &json_data).await;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let _ = cache
.write(meta_key, timestamp.to_string().as_bytes())
.await;
}
Ok(builds)
}
pub fn filter_builds_by_product(builds: WagoBuildsResponse, product: &str) -> Vec<WagoBuild> {
match builds {
WagoBuildsResponse::Map(map) => map.get(product).cloned().unwrap_or_default(),
WagoBuildsResponse::Array(builds) => builds
.into_iter()
.filter(|b| b.product == product)
.collect(),
}
}
pub fn extract_build_id(version: &str) -> Option<String> {
version.split('.').next_back().map(|s| s.to_string())
}
pub fn find_build_by_id<'a>(builds: &'a [WagoBuild], build_id: &str) -> Option<&'a WagoBuild> {
builds.iter().find(|build| {
if let Some(extracted_id) = extract_build_id(&build.version) {
extracted_id == build_id
} else {
build.version == build_id
}
})
}
pub fn parse_wago_date(date_str: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_str(&format!("{date_str} +00:00"), "%Y-%m-%d %H:%M:%S %z")
.ok()
.map(|dt| dt.with_timezone(&Utc))
}