summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-01-09 23:34:40 -0800
committerFuwn <[email protected]>2024-01-09 23:34:40 -0800
commitfa9f5f70a5221bb940354f3e4d978d4312af77e4 (patch)
tree06a8fd1e6c254fae373459a606bf3b256ccb8efe /src
downloadrin-fa9f5f70a5221bb940354f3e4d978d4312af77e4.tar.xz
rin-fa9f5f70a5221bb940354f3e4d978d4312af77e4.zip
feat: initial release
Diffstat (limited to 'src')
-rw-r--r--src/main.rs76
-rw-r--r--src/schedule.rs175
-rw-r--r--src/web.rs27
-rw-r--r--src/week.rs22
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(&current_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 }
+}