MVP achieved.

This commit is contained in:
Vegard Berg 2023-01-25 08:37:35 +01:00
parent 25b64a6ca0
commit 390193db14
3 changed files with 220 additions and 38 deletions

8
Cargo.lock generated
View File

@ -80,6 +80,7 @@ dependencies = [
"js-sys", "js-sys",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
"time", "time",
"wasm-bindgen", "wasm-bindgen",
"winapi", "winapi",
@ -372,6 +373,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.139" version = "0.2.139"
@ -420,6 +427,7 @@ dependencies = [
"clap", "clap",
"dirs", "dirs",
"html-escape", "html-escape",
"lazy_static",
"owo-colors", "owo-colors",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -5,12 +5,20 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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] [dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
chrono = "0.4.23" chrono = { version = "0.4.23", features = ["serde"] }
clap = { version = "4.1.2", features = ["derive"] } clap = { version = "4.1.2", features = ["derive"] }
dirs = "4.0.0" dirs = "4.0.0"
html-escape = "0.2.13" html-escape = "0.2.13"
lazy_static = "1.4.0"
owo-colors = { version = "3.5.0", features = ["supports-colors"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] }
serde = { version = "1.0.152", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91" serde_json = "1.0.91"

View File

@ -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 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)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@ -17,9 +31,11 @@ struct Cli {
/// Colorise output. No effect when --json is set. /// Colorise output. No effect when --json is set.
#[arg(short, long, default_value_t = ColoriseOutput::Auto)] #[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)] #[derive(strum::Display, Debug, ValueEnum, Clone, Copy)]
@ -30,26 +46,161 @@ pub enum ColoriseOutput {
False, False,
} }
fn main() -> anyhow::Result<()>{ const MENU_URL: &'static str =
let _args = Cli::parse(); "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=";
let resp = ureq::get("http://kantinemeny.azurewebsites.net/ukesmeny?lokasjon=toro@albatross-as.no&dato=") fn main() -> anyhow::Result<()> {
.call()?; let args = Cli::parse();
let body = resp.into_string()?; let now = chrono::offset::Utc::now();
let weekday_offset = now
.weekday()
.num_days_from_monday();
let items = body.html_string_to_vec()?; 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()?,
};
for item in items { set_cached_data(&data)?;
println!("{} - {}", item.title.unwrap_or("".to_string()), item.additional.unwrap_or("".to_string())); 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 items = cond!(args.soup => cache.soup_items; cache.items);
match args.color {
ColoriseOutput::True => {
std::env::set_var("FORCE_COLOR", "true");
}
ColoriseOutput::False => {
std::env::set_var("NO_COLOR", "false");
}
_ => {}
};
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(); if args.json {
println!("{}", now.date_naive().weekday().number_from_monday()); let json =serde_json::to_string_pretty(&items)?;
println!("{}", json);
return Ok(())
}
display_week(items, weekday_offset);
Ok(()) Ok(())
} }
fn get_cached_data() -> anyhow::Result<CachedData> {
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<MenuItem>, 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 { trait DeocdeHTMLEnts {
fn decode_html_ents(&self) -> Self; fn decode_html_ents(&self) -> Self;
} }
@ -68,25 +219,25 @@ trait HTMLStringToVec {
fn get_item(p: &tl::Parser, tag: &tl::HTMLTag) -> Option<String> { fn get_item(p: &tl::Parser, tag: &tl::HTMLTag) -> Option<String> {
Some( Some(
tag tag.query_selector(p, ".dagsrett")?
.query_selector(p, ".dagsrett")?
.last()? .last()?
.get(p)? .get(p)?
.as_tag()? .as_tag()?
.inner_text(p).to_string() .inner_text(p)
.decode_html_ents() .to_string()
.decode_html_ents(),
) )
} }
fn get_item_additional(p: &tl::Parser, tag: &tl::HTMLTag) -> Option<String> { fn get_item_additional(p: &tl::Parser, tag: &tl::HTMLTag) -> Option<String> {
Some( Some(
tag tag.query_selector(p, ".dagsrettgarnityr")?
.query_selector(p, ".dagsrettgarnityr")?
.last()? .last()?
.get(p)? .get(p)?
.as_tag()?.inner_text(p).to_string() .as_tag()?
.decode_html_ents() .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 dom = tl::parse(self, tl::ParserOptions::default())?;
let p = dom.parser(); 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 items = nodes
let tag = i.as_tag().unwrap(); .into_iter()
let text = get_item(p, tag); .map(|i| {
let tag = i.as_tag().unwrap();
let title = get_item(p, tag);
let additional = get_item_additional(p, tag); let additional = get_item_additional(p, tag);
MenuItem { MenuItem {
title: text, title,
additional: additional, additional,
} }
}).collect::<Vec<MenuItem>>(); })
.collect::<Vec<MenuItem>>();
Ok(items) Ok(items)
} }
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
struct MenuItem { struct MenuItem {
pub title: Option<String>, pub title: Option<String>,
pub additional: Option<String>, pub additional: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)]
struct CachedData {
#[serde(with = "ts_seconds")]
pub timestamp: chrono::DateTime<Utc>,
pub items: Vec<MenuItem>,
pub soup_items: Vec<MenuItem>,
}