From ebda2117d53ec52648a13651210a95ff3df3f62f Mon Sep 17 00:00:00 2001 From: Matthew Ryan Dillon Date: Sun, 11 May 2025 07:46:52 -0400 Subject: [PATCH] basic caching --- Cargo.lock | 1 + Cargo.toml | 2 +- src/cache.rs | 52 ++++++++++++++++++++++++++++++++++++++++ src/calendar.rs | 2 ++ src/config.rs | 3 +++ src/main.rs | 63 ++++++++++++++++++++++++++++++++++++++----------- src/pollen.rs | 4 +++- src/weather.rs | 3 ++- 8 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/cache.rs diff --git a/Cargo.lock b/Cargo.lock index 3b5cfcf..e74fda1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] diff --git a/Cargo.toml b/Cargo.toml index 2d61a0a..51d63b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ toml = "0.8" # Only keep one of blocking+async if all features converted reqwest = { version = "0.12", features = ["json", "rustls-tls" ] } dirs = "5" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } serde_json = "1.0" chrono-tz = "0.8" tokio = { version = "1.37", features = ["full"] } diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..dbc43df --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Serialize, Deserialize)] +pub struct Cache { + pub timestamp: u64, + pub data: T, +} + +fn get_cache_path(report_type: &str) -> PathBuf { + let home = std::env::var("HOME").expect("HOME not set"); + let cache_dir = PathBuf::from(format!("{}/.local/state/trmnl", home)); + let _ = fs::create_dir_all(&cache_dir); + cache_dir.join(format!("{}.json", report_type)) +} + +use serde::de::DeserializeOwned; +pub fn load_cache( + report_type: &str, + expiry_secs: u64, +) -> Option { + let path = get_cache_path(report_type); + let contents = fs::read_to_string(&path).ok()?; + let parsed: Cache = serde_json::from_str(&contents).ok()?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + if now - parsed.timestamp <= expiry_secs { + println!("(using cached {} report)", report_type); + Some(parsed.data) + } else { + None + } +} + +pub fn save_cache(report_type: &str, data: &T) { + let path = get_cache_path(report_type); + let to_save = Cache { + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + data: data, + }; + let json = serde_json::to_string_pretty(&to_save).unwrap(); + let _ = fs::write(path, json); +} diff --git a/src/calendar.rs b/src/calendar.rs index e7a168b..6a6e30c 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -1,8 +1,10 @@ use chrono::{DateTime, FixedOffset, NaiveDate, Offset, TimeZone, Utc}; use ical::IcalParser; use rrule::{RRule, RRuleSet, Unvalidated}; +use serde::{Deserialize, Serialize}; use std::error::Error; +#[derive(Serialize, Deserialize)] pub struct CalendarEventSummary { pub start: Option>, pub summary: String, diff --git a/src/config.rs b/src/config.rs index 2cba554..8275378 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,9 @@ use std::path::PathBuf; #[derive(Debug, Deserialize)] pub struct Config { + pub cache_weather_secs: Option, + pub cache_pollen_secs: Option, + pub cache_calendar_secs: Option, pub weather_api_key: String, pub pollen_api_key: String, pub calendar_api_key: String, diff --git a/src/main.rs b/src/main.rs index 39c72b9..de6efad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod cache; mod calendar; mod config; mod pollen; @@ -20,12 +21,21 @@ async fn main() { } }; + // Weather - with cache + let cache_weather_secs = config.cache_weather_secs.unwrap_or(300); let weather: Option = - match fetch_weather(&config.location, &config.weather_api_key).await { - Ok(data) => Some(data), - Err(e) => { - eprintln!("Failed to fetch weather: {e}"); - None + if let Some(w) = cache::load_cache::("weather", cache_weather_secs) { + Some(w) + } else { + match fetch_weather(&config.location, &config.weather_api_key).await { + Ok(data) => { + cache::save_cache("weather", &data); + Some(data) + } + Err(e) => { + eprintln!("Failed to fetch weather: {e}"); + None + } } }; let (_lat, _lon) = if weather.is_some() { @@ -66,7 +76,19 @@ async fn main() { println!("Weather: N/A"); } - match fetch_pollen_api(&config.pollen_zip).await { + // Pollen - with cache + let cache_pollen_secs = config.cache_pollen_secs.unwrap_or(300); + match if let Some(p) = cache::load_cache::("pollen", cache_pollen_secs) { + Ok(p) + } else { + match fetch_pollen_api(&config.pollen_zip).await { + Ok(data) => { + cache::save_cache("pollen", &data); + Ok(data) + } + Err(e) => Err(e), + } + } { Ok(p) => { if let Ok(dt) = DateTime::parse_from_rfc3339(&p.forecast_date) { match config.timezone.parse::() { @@ -110,16 +132,29 @@ async fn main() { } } - println!(); - println!("Upcoming calendar events:"); + // Calendar - with cache + let cache_calendar_secs = config.cache_calendar_secs.unwrap_or(300); + println!("\nUpcoming calendar events:"); - match fetch_next_events( - &config.calendar_url, - config.calendar_event_count, - &config.timezone, - ) - .await + match if let Some(events) = + cache::load_cache::>("calendar", cache_calendar_secs) { + Ok(events) + } else { + match fetch_next_events( + &config.calendar_url, + config.calendar_event_count, + &config.timezone, + ) + .await + { + Ok(data) => { + cache::save_cache("calendar", &data); + Ok(data) + } + Err(e) => Err(e), + } + } { Ok(events) if !events.is_empty() => { for (i, event) in events.iter().enumerate() { let day = event diff --git a/src/pollen.rs b/src/pollen.rs index 1f2f916..6b1222b 100644 --- a/src/pollen.rs +++ b/src/pollen.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::error::Error; @@ -34,11 +34,13 @@ struct Trigger { genus: String, } +#[derive(Serialize, Deserialize)] pub struct PollenPeriod { pub index: f32, pub triggers: Vec, } +#[derive(Serialize, Deserialize)] pub struct PollenSummary { pub location: String, pub forecast_date: String, diff --git a/src/weather.rs b/src/weather.rs index 05c0dbb..1cb53e7 100644 --- a/src/weather.rs +++ b/src/weather.rs @@ -1,4 +1,4 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::error::Error; #[allow(dead_code)] @@ -72,6 +72,7 @@ pub struct OneCallResult { pub daily: Vec, } +#[derive(Serialize, Deserialize)] pub struct WeatherSummary { pub temp: String, pub current_desc: String,