basic caching

This commit is contained in:
Matthew Ryan Dillon 2025-05-11 07:46:52 -04:00
parent 139528fccf
commit ebda2117d5
8 changed files with 113 additions and 17 deletions

1
Cargo.lock generated
View file

@ -142,6 +142,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]

View file

@ -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"] }

52
src/cache.rs Normal file
View file

@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
use std::{
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[derive(Serialize, Deserialize)]
pub struct Cache<T> {
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<T: Serialize + DeserializeOwned>(
report_type: &str,
expiry_secs: u64,
) -> Option<T> {
let path = get_cache_path(report_type);
let contents = fs::read_to_string(&path).ok()?;
let parsed: Cache<T> = 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<T: Serialize>(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);
}

View file

@ -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<DateTime<FixedOffset>>,
pub summary: String,

View file

@ -5,6 +5,9 @@ use std::path::PathBuf;
#[derive(Debug, Deserialize)]
pub struct Config {
pub cache_weather_secs: Option<u64>,
pub cache_pollen_secs: Option<u64>,
pub cache_calendar_secs: Option<u64>,
pub weather_api_key: String,
pub pollen_api_key: String,
pub calendar_api_key: String,

View file

@ -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<WeatherSummary> =
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::<WeatherSummary>("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::PollenSummary>("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::<Tz>() {
@ -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::<Vec<calendar::CalendarEventSummary>>("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

View file

@ -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<String>,
}
#[derive(Serialize, Deserialize)]
pub struct PollenSummary {
pub location: String,
pub forecast_date: String,

View file

@ -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<WeatherDaily>,
}
#[derive(Serialize, Deserialize)]
pub struct WeatherSummary {
pub temp: String,
pub current_desc: String,