diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..ce3c7ca --- /dev/null +++ b/src/data.rs @@ -0,0 +1,92 @@ +use std::{path::PathBuf, io::Write}; + +use chrono::{serde::ts_seconds, Utc, DateTime}; +use serde::{Serialize, Deserialize}; + +use crate::html::HTMLStringToVec; + + +pub(crate) 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) +} + +pub(crate) 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(()) +} + +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="; + +pub(crate) fn get_cached_or_fetch_data(now: DateTime) -> anyhow::Result { + 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 + } + }; + + Ok(cache) +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct MenuItem { + pub title: Option, + pub additional: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct CachedData { + #[serde(with = "ts_seconds")] + pub timestamp: chrono::DateTime, + pub items: Vec, + pub soup_items: Vec, +} diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..72d0eb6 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,71 @@ +use crate::data::MenuItem; + + +trait DeocdeHTMLEnts { + fn decode_html_ents(&self) -> Self; +} + +impl DeocdeHTMLEnts for String { + fn decode_html_ents(&self) -> Self { + let mut s = String::new(); + html_escape::decode_html_entities_to_string(self, &mut s); + s + } +} +pub(crate) trait HTMLStringToVec { + fn html_string_to_vec(&self) -> anyhow::Result>; +} + + +fn get_item(p: &tl::Parser, tag: &tl::HTMLTag) -> Option { + Some( + tag.query_selector(p, ".dagsrett")? + .last()? + .get(p)? + .as_tag()? + .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")? + .last()? + .get(p)? + .as_tag()? + .inner_text(p) + .to_string() + .decode_html_ents(), + ) +} +impl HTMLStringToVec for String { + fn html_string_to_vec(&self) -> anyhow::Result> { + 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 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, + additional, + } + }) + .collect::>(); + + Ok(items) + } +} diff --git a/src/main.rs b/src/main.rs index e34f0aa..a8e4b31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ +mod html; +mod data; + +use crate::data::{get_cached_or_fetch_data, MenuItem}; 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 chrono::Datelike; use clap::{Parser, ValueEnum}; use lazy_static::lazy_static; @@ -46,10 +45,6 @@ pub enum ColoriseOutput { False, } -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(); @@ -59,41 +54,11 @@ fn main() -> anyhow::Result<()> { .weekday() .num_days_from_monday(); - 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 Ok(data) = get_cached_or_fetch_data(now) else { + panic!("Failed to get data"); }; - - let items = cond!(args.soup => cache.soup_items; cache.items); + + let items = cond!(args.soup => data.soup_items; data.items); match args.color { ColoriseOutput::True => { @@ -106,33 +71,15 @@ fn main() -> anyhow::Result<()> { }; 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); + display_today(items, weekday_offset); return Ok(()) } if args.json { - let json =serde_json::to_string_pretty(&items)?; + let Ok(json) = serde_json::to_string_pretty(&items) else { + panic!("Failed to convert into JSON.") + }; println!("{}", json); return Ok(()) @@ -143,33 +90,6 @@ fn main() -> anyhow::Result<()> { 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> = @@ -201,86 +121,29 @@ fn display_week(items: Vec, day: u32) { } } -trait DeocdeHTMLEnts { - fn decode_html_ents(&self) -> Self; +fn display_today(items: Vec, weekday_offset: u32) { + + if weekday_offset as usize >= (&items).len() { + return + } + + 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); } -impl DeocdeHTMLEnts for String { - fn decode_html_ents(&self) -> Self { - let mut s = String::new(); - html_escape::decode_html_entities_to_string(self, &mut s); - s - } -} -trait HTMLStringToVec { - fn html_string_to_vec(&self) -> anyhow::Result>; -} - -fn get_item(p: &tl::Parser, tag: &tl::HTMLTag) -> Option { - Some( - tag.query_selector(p, ".dagsrett")? - .last()? - .get(p)? - .as_tag()? - .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")? - .last()? - .get(p)? - .as_tag()? - .inner_text(p) - .to_string() - .decode_html_ents(), - ) -} - -impl HTMLStringToVec for String { - fn html_string_to_vec(&self) -> anyhow::Result> { - 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 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, - additional, - } - }) - .collect::>(); - - Ok(items) - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct MenuItem { - pub title: Option, - pub additional: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CachedData { - #[serde(with = "ts_seconds")] - pub timestamp: chrono::DateTime, - pub items: Vec, - pub soup_items: Vec, -}