use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use std::time::Instant;
use tracing::{debug, info, warn};
pub struct ArticleCacheEntry {
pub content: String,
pub timestamp: Instant,
pub etag: Option<String>,
pub last_modified: Option<String>,
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct ArticleDiskCacheEntry {
pub content: String,
pub saved_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub etag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_modified: Option<String>,
}
pub static ARTICLE_CACHE: LazyLock<Mutex<HashMap<String, ArticleCacheEntry>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub const ARTICLE_CACHE_TTL_SECONDS: u64 = 900;
pub fn article_disk_cache_ttl_seconds() -> i64 {
let days = crate::theme::settings().news_cache_ttl_days.max(1);
i64::from(days) * 86400 }
pub fn article_disk_cache_path() -> std::path::PathBuf {
crate::theme::lists_dir().join("news_article_cache.json")
}
pub fn load_article_entry_from_disk_cache(url: &str) -> Option<ArticleDiskCacheEntry> {
let path = article_disk_cache_path();
let content = std::fs::read_to_string(&path).ok()?;
let cache: HashMap<String, ArticleDiskCacheEntry> = serde_json::from_str(&content).ok()?;
let entry = cache.get(url)?.clone();
let now = chrono::Utc::now().timestamp();
let age = now - entry.saved_at;
let ttl = article_disk_cache_ttl_seconds();
if age < ttl {
info!(
url,
age_hours = age / 3600,
ttl_days = ttl / 86400,
"loaded article from disk cache"
);
Some(entry)
} else {
debug!(
url,
age_hours = age / 3600,
ttl_days = ttl / 86400,
"article disk cache expired"
);
None
}
}
pub fn save_article_to_disk_cache(
url: &str,
content: &str,
etag: Option<String>,
last_modified: Option<String>,
) {
let path = article_disk_cache_path();
let mut cache: HashMap<String, ArticleDiskCacheEntry> = std::fs::read_to_string(&path)
.map_or_else(
|_| HashMap::new(),
|file_content| serde_json::from_str(&file_content).unwrap_or_default(),
);
cache.insert(
url.to_string(),
ArticleDiskCacheEntry {
content: content.to_string(),
saved_at: chrono::Utc::now().timestamp(),
etag,
last_modified,
},
);
match serde_json::to_string_pretty(&cache) {
Ok(json) => {
if let Err(e) = std::fs::write(&path, json) {
warn!(error = %e, url, "failed to write article disk cache");
} else {
debug!(url, "saved article to disk cache");
}
}
Err(e) => warn!(error = %e, url, "failed to serialize article disk cache"),
}
}