From 390193db14110ed600d8636b5325c003ee0af6c9 Mon Sep 17 00:00:00 2001 From: Vegard Berg Date: Wed, 25 Jan 2023 08:37:35 +0100 Subject: [PATCH] MVP achieved. --- Cargo.lock | 8 ++ Cargo.toml | 10 ++- src/main.rs | 240 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 220 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 050e7c8..bfb91c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits", + "serde", "time", "wasm-bindgen", "winapi", @@ -372,6 +373,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.139" @@ -420,6 +427,7 @@ dependencies = [ "clap", "dirs", "html-escape", + "lazy_static", "owo-colors", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index a1c1d37..fef336b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,20 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + [dependencies] anyhow = "1.0.68" -chrono = "0.4.23" +chrono = { version = "0.4.23", features = ["serde"] } clap = { version = "4.1.2", features = ["derive"] } dirs = "4.0.0" html-escape = "0.2.13" +lazy_static = "1.4.0" owo-colors = { version = "3.5.0", features = ["supports-colors"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" diff --git a/src/main.rs b/src/main.rs index 5d1f445..e34f0aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,22 @@ -use std::string::ToString; +use owo_colors::{AnsiColors, OwoColorize, Stream::Stdout}; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, io::Write}; +use dirs; + +use chrono::{serde::ts_seconds, Datelike, Utc}; use clap::{Parser, ValueEnum}; -use chrono::Datelike; +use lazy_static::lazy_static; +macro_rules! cond { + ($cond:expr => $true:expr; $false:expr) => { + if $cond { + $true + } else { + $false + } + }; +} #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -17,9 +31,11 @@ struct Cli { /// Colorise output. No effect when --json is set. #[arg(short, long, default_value_t = ColoriseOutput::Auto)] - pub color: ColoriseOutput - + pub color: ColoriseOutput, + /// Output today only. Incompatible with --color + #[arg(short, long)] + pub today: bool, } #[derive(strum::Display, Debug, ValueEnum, Clone, Copy)] @@ -30,26 +46,161 @@ pub enum ColoriseOutput { False, } -fn main() -> anyhow::Result<()>{ - let _args = Cli::parse(); +const MENU_URL: &'static str = + "http://kantinemeny.azurewebsites.net/ukesmeny?lokasjon=toro@albatross-as.no&dato="; +const SOUP_URL: &'static str = + "http://kantinemeny.azurewebsites.net/ukesmenysuppe?lokasjon=toro@albatross-as.no&dato="; + +fn main() -> anyhow::Result<()> { + let args = Cli::parse(); + + let now = chrono::offset::Utc::now(); + let weekday_offset = now + .weekday() + .num_days_from_monday(); - let resp = ureq::get("http://kantinemeny.azurewebsites.net/ukesmeny?lokasjon=toro@albatross-as.no&dato=") - .call()?; + let cache = match get_cached_data() { + Ok(data) => { + if (now - data.timestamp).num_minutes() > 60 { + let data = CachedData { + timestamp: now.clone(), + items: ureq::get(MENU_URL).call()? + .into_string()? + .html_string_to_vec()?, + soup_items: ureq::get(SOUP_URL).call()? + .into_string()? + .html_string_to_vec()?, + }; + + set_cached_data(&data)?; + data + } else { + data + } + }, + Err(_) => { + let data = CachedData { + timestamp: now.clone(), + items: ureq::get(MENU_URL).call()? + .into_string()? + .html_string_to_vec()?, + soup_items: ureq::get(SOUP_URL).call()? + .into_string()? + .html_string_to_vec()?, + }; + set_cached_data(&data)?; + data + } + }; - let body = resp.into_string()?; + let items = cond!(args.soup => cache.soup_items; cache.items); - let items = body.html_string_to_vec()?; + match args.color { + ColoriseOutput::True => { + std::env::set_var("FORCE_COLOR", "true"); + } + ColoriseOutput::False => { + std::env::set_var("NO_COLOR", "false"); + } + _ => {} + }; - for item in items { - println!("{} - {}", item.title.unwrap_or("".to_string()), item.additional.unwrap_or("".to_string())); + if args.today { + if weekday_offset as usize >= (&items).len() { + return Ok(()) + } + + let item = &items[weekday_offset as usize]; + + let title: String = match item.title.clone() { + Some(t) => t, + None => String::new(), + }; + + let additional = item.additional.clone().unwrap_or_default(); + + let mut final_s = String::new(); + final_s.push_str(&title); + if !additional.is_empty() { + let f = format!(" - {}", additional); + final_s.push_str(&f); + } + + println!("{}", final_s); + + return Ok(()) } - - let now = chrono::offset::Local::now(); - println!("{}", now.date_naive().weekday().number_from_monday()); + + if args.json { + let json =serde_json::to_string_pretty(&items)?; + println!("{}", json); + + return Ok(()) + } + + display_week(items, weekday_offset); Ok(()) } +fn get_cached_data() -> anyhow::Result { + let mut cache = dirs::cache_dir().unwrap_or(PathBuf::from(".")); + cache.push("n58-kantine"); + cache.push("data.json"); + + let s = std::fs::read_to_string(cache)?; + let cached_data: CachedData = serde_json::from_str(&s)?; + + Ok(cached_data) +} + +fn set_cached_data(data: &CachedData) -> anyhow::Result<()> { + let mut cache = dirs::cache_dir().unwrap_or(PathBuf::from(".")); + cache.push("n58-kantine"); + let dir = cache.clone(); + cache.push("data.json"); + + let json = serde_json::to_string_pretty(data)?; + + std::fs::create_dir_all(dir)?; + + let mut f = std::fs::File::create(cache)?; + f.write_all(json.as_bytes())?; + + + Ok(()) +} + +lazy_static! { + static ref WEEKDAYS: Vec<&'static str> = + vec!["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; +} + +fn display_week(items: Vec, day: u32) { + for (i, item) in items.into_iter().enumerate() { + if i >= WEEKDAYS.len() { + break; + }; + + let is_today = day == (i as u32); + + println!( + "{day}\n {title}\n {additional}", + day = format!( + "{}{}{}", + "[".if_supports_color(Stdout, |s| s.color(cond!(is_today => AnsiColors::BrightWhite; AnsiColors::BrightBlack))), + WEEKDAYS[i].if_supports_color(Stdout, |s| s.color(AnsiColors::Green)), + "]".if_supports_color(Stdout, |s| s.color(cond!(is_today => AnsiColors::BrightWhite; AnsiColors::BrightBlack))), + ), + title = item.title.unwrap_or("".to_owned()), + additional = match item.additional { + Some(text) => format!("{}\n", text), + None => format!(""), + }, + ); + } +} + trait DeocdeHTMLEnts { fn decode_html_ents(&self) -> Self; } @@ -68,25 +219,25 @@ trait HTMLStringToVec { fn get_item(p: &tl::Parser, tag: &tl::HTMLTag) -> Option { Some( - tag - .query_selector(p, ".dagsrett")? + tag.query_selector(p, ".dagsrett")? .last()? .get(p)? .as_tag()? - .inner_text(p).to_string() - .decode_html_ents() + .inner_text(p) + .to_string() + .decode_html_ents(), ) - } fn get_item_additional(p: &tl::Parser, tag: &tl::HTMLTag) -> Option { Some( - tag - .query_selector(p, ".dagsrettgarnityr")? + tag.query_selector(p, ".dagsrettgarnityr")? .last()? .get(p)? - .as_tag()?.inner_text(p).to_string() - .decode_html_ents() + .as_tag()? + .inner_text(p) + .to_string() + .decode_html_ents(), ) } @@ -95,26 +246,41 @@ impl HTMLStringToVec for String { let dom = tl::parse(self, tl::ParserOptions::default())?; let p = dom.parser(); - let nodes: Vec<&tl::Node> = Vec::from_iter(dom.query_selector(".dagsinfo").unwrap().filter_map(|x| x.get(p))); + let nodes: Vec<&tl::Node> = Vec::from_iter( + dom.query_selector(".dagsinfo") + .unwrap() + .filter_map(|x| x.get(p)), + ); - let items = nodes.into_iter().map(|i| { - let tag = i.as_tag().unwrap(); - let text = get_item(p, tag); + let items = nodes + .into_iter() + .map(|i| { + let tag = i.as_tag().unwrap(); + let title = get_item(p, tag); - let additional = get_item_additional(p, tag); - - MenuItem { - title: text, - additional: additional, - } - }).collect::>(); + let additional = get_item_additional(p, tag); + + MenuItem { + title, + additional, + } + }) + .collect::>(); Ok(items) } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] struct MenuItem { pub title: Option, pub additional: Option, -} \ No newline at end of file +} + +#[derive(Debug, Serialize, Deserialize)] +struct CachedData { + #[serde(with = "ts_seconds")] + pub timestamp: chrono::DateTime, + pub items: Vec, + pub soup_items: Vec, +}