diff options
| author | Fuwn <[email protected]> | 2024-01-09 23:34:40 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-01-09 23:34:40 -0800 |
| commit | fa9f5f70a5221bb940354f3e4d978d4312af77e4 (patch) | |
| tree | 06a8fd1e6c254fae373459a606bf3b256ccb8efe /src | |
| download | rin-fa9f5f70a5221bb940354f3e4d978d4312af77e4.tar.xz rin-fa9f5f70a5221bb940354f3e4d978d4312af77e4.zip | |
feat: initial release
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 76 | ||||
| -rw-r--r-- | src/schedule.rs | 175 | ||||
| -rw-r--r-- | src/web.rs | 27 | ||||
| -rw-r--r-- | src/week.rs | 22 |
4 files changed, 300 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..38c9103 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,76 @@ +#![deny( + warnings, + nonstandard_style, + unused, + future_incompatible, + rust_2018_idioms, + unsafe_code +)] +#![deny(clippy::all, clippy::nursery, clippy::pedantic)] +#![recursion_limit = "128"] +#![allow(clippy::cast_precision_loss)] + +use thirtyfour::error::WebDriverResult; + +mod schedule; +mod web; +mod week; + +#[derive(serde::Serialize, serde::Deserialize)] +struct Cache { + schedule: schedule::MarkdownMap, + last_updated: std::time::SystemTime, +} + +#[tokio::main] +async fn main() -> WebDriverResult<()> { + let cache = format!("{}/rin_cache", std::env::temp_dir().display()); + + if std::path::Path::new(&cache).exists() + && std::env::var("RIN_CACHE_BUST").unwrap_or_else(|_| "0".to_string()) + != "1" + { + let schedule: Cache = serde_json::from_str( + &std::fs::read_to_string(&cache).expect("failed to read cache"), + ) + .expect("failed to parse cache"); + + if schedule + .last_updated + .elapsed() + .unwrap() + .as_secs() + .checked_div(60) + .unwrap() + .checked_div(60) + .unwrap() + < 24 + { + schedule::render_markdown(&schedule.schedule).await; + + return Ok(()); + } + } + + let mut geckodriver = + web::geckodriver().expect("failed to start geckodriver"); + let driver = web::webdriver().await.expect("failed to start webdriver"); + let markdown = + schedule::to_markdown(&driver).await.expect("failed to get schedule"); + let schedule = schedule::markdown_to_map(&markdown); + + std::fs::write( + cache, + serde_json::to_string(&Cache { + schedule: schedule.clone(), + last_updated: std::time::SystemTime::now(), + }) + .unwrap(), + ) + .unwrap(); + schedule::render_markdown(&schedule).await; + driver.quit().await.expect("failed to quit webdriver"); + geckodriver.kill().await.expect("failed to kill geckodriver"); + + Ok(()) +} diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..c6836cc --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,175 @@ +use { + crate::week::DAYS_OF_WEEK, + std::collections::HashMap, + thirtyfour::prelude::{ElementQueryable, ElementWaitable}, + tokio::io::AsyncWriteExt, +}; + +pub type MarkdownMap = HashMap<String, Vec<String>>; + +pub async fn to_markdown( + driver: &thirtyfour::WebDriver, +) -> Result<String, thirtyfour::error::WebDriverError> { + let schedule = driver + .query(thirtyfour::By::Id("schedule")) + .first() + .await + .expect("failed to find schedule"); + + schedule + .wait_until() + .displayed() + .await + .expect("failed to wait for schedule to be displayed"); + + let mut markdown = html2md::parse_html( + &schedule.inner_html().await.expect("failed to get schedule inner html"), + ); + + markdown = markdown + .lines() + .map(|mut line| { + if line.contains("<summary>") { + line = line.split("<summary>").collect::<Vec<&str>>()[1]; + line = line.split("</summary>").collect::<Vec<&str>>()[0]; + + format!("# {line}") + } else { + line.to_string() + } + }) + .filter(|line| { + !line.starts_with("<details") && !line.starts_with("</details>") + }) + .collect::<Vec<String>>() + .join("\n"); + + Ok(markdown) +} + +pub async fn print_markdown_pipe_markdown( + markdown: &str, +) -> Result<(), std::io::Error> { + let mut child = tokio::process::Command::new("mdcat") + .env( + "TERM", + std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".to_string()), + ) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("failed to spawn mdcat"); + + child + .stdin + .as_mut() + .unwrap() + .write_all(markdown.as_bytes()) + .await + .expect("failed to write to mdcat"); + + let child_output = child.wait_with_output().await.unwrap(); + let output = String::from_utf8_lossy(&child_output.stdout).to_string(); + + println!(); + + for line in output.lines() { + println!(" {line}"); + } + + println!(); + + Ok(()) +} + +pub fn markdown_to_map(markdown: &str) -> MarkdownMap { + let mut schedule = HashMap::new(); + let mut current_day = String::new(); + + for line in markdown.lines() { + if line.starts_with("# ") { + current_day = line.replace("# ", "").to_string(); + + schedule.insert(current_day.clone(), Vec::new()); + } else if !line.is_empty() { + schedule.get_mut(¤t_day).unwrap().push(line.to_string()); + } + } + + schedule +} + +#[allow(clippy::future_not_send)] +pub async fn render_markdown(schedule: &MarkdownMap) { + let mut args = std::env::args(); + + args.next(); + + let mut day = args.next().unwrap_or_default(); + + if !day.is_empty() && !DAYS_OF_WEEK.contains(&day.as_str()) { + let mut closest_match = String::new(); + + for day_of_week in DAYS_OF_WEEK { + if day_of_week.to_lowercase().starts_with(&day.to_lowercase()) { + closest_match = day_of_week.to_string(); + + break; + } + } + + if closest_match.is_empty() { + day = String::new(); + } else { + day = closest_match; + } + } + + let mut days = Vec::new(); + + for (day, media) in schedule { + days.push(crate::week::Day::new(day.to_string(), media.clone())); + } + + days.sort_by(|a, b| { + DAYS_OF_WEEK + .iter() + .position(|&day| day == a.name()) + .unwrap() + .cmp(&DAYS_OF_WEEK.iter().position(|&day| day == b.name()).unwrap()) + }); + + let today = chrono::Local::now().format("%A").to_string(); + + if let Some(index) = days.iter().position(|day| day.name() == today) { + days.rotate_left(index); + } + + days.reverse(); + + if day.is_empty() { + for d in days { + let mut lines = String::new(); + + lines.push_str(&format!("{}\n", d.name())); + + for line in d.media() { + lines.push_str(line); + lines.push('\n'); + } + + print_markdown_pipe_markdown(&lines).await.unwrap(); + } + } else { + let mut lines = String::new(); + + lines.push_str(&format!("{day}\n\n")); + + for line in schedule.get(&day).unwrap() { + lines.push_str(line); + lines.push('\n'); + } + + print_markdown_pipe_markdown(&lines).await.unwrap(); + } +} diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 0000000..eb10ff2 --- /dev/null +++ b/src/web.rs @@ -0,0 +1,27 @@ +use {thirtyfour::WebDriver, tokio::process::Command}; + +pub fn geckodriver() -> Result<tokio::process::Child, std::io::Error> { + Command::new("geckodriver") + .arg("--port=9515") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() +} + +pub async fn webdriver() -> Result<WebDriver, thirtyfour::error::WebDriverError> +{ + let mut caps = thirtyfour::DesiredCapabilities::firefox(); + + caps.set_headless().expect("failed to set headless"); + + let driver = WebDriver::new("http://localhost:9515", caps) + .await + .expect("failed to connect to webdriver"); + + driver + .goto("https://due.moe/schedule") + .await + .expect("failed to navigate to https://due.moe/schedule"); + + Ok(driver) +} diff --git a/src/week.rs b/src/week.rs new file mode 100644 index 0000000..f45b4d2 --- /dev/null +++ b/src/week.rs @@ -0,0 +1,22 @@ +pub const DAYS_OF_WEEK: [&str; 7] = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +]; + +pub struct Day { + name: String, + media: Vec<String>, +} + +impl Day { + pub fn new(name: String, media: Vec<String>) -> Self { Self { name, media } } + + pub fn name(&self) -> &str { &self.name } + + pub const fn media(&self) -> &Vec<String> { &self.media } +} |