diff --git a/src/main.rs b/src/main.rs index b601fb2..12688de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,6 @@ struct Config { common_ninjas: Vec, icals: Vec, output: output::Config, - feeds: Vec, } impl Config { @@ -53,11 +52,10 @@ impl Config { .chain(self.icals.iter().map(|ical| ical.dl.clone())) } - fn upstream_calendars(&self) -> Vec { + fn upstreams(&self) -> Vec { let Self { campfires, common_ninjas, - feeds: _, icals, output: _, } = self; @@ -163,7 +161,7 @@ struct Data { icals: Vec, } -fn read_calendars(config: &Config) -> Result { +fn read_data_from_disk(config: &Config) -> Result { Ok(Data { campfires: config .campfires @@ -258,19 +256,9 @@ async fn do_everything(cli: &CliAuto) -> Result<()> { std::fs::rename(&temp_path, &dl.file_path)?; } - let events = { - let cal_data = read_calendars(&config)?; - process_data(&cal_data, &config.output, now)? - }; - let feed_items = read_feeds(&config)?; - - output::write_html( - &config.output, - &feed_items, - &config.upstream_calendars(), - &events, - now, - )?; + let data = read_data_from_disk(&config)?; + let instances = process_data(&data, &config.output, now)?; + output::write_html(&config.output, &config.upstreams(), &instances, now)?; Ok(()) } @@ -292,40 +280,31 @@ fn main_auto(cli: CliAuto) -> Result<()> { } #[derive(clap::Parser)] -struct CliDebugEvents { +struct CliDebugOutput { #[arg(long)] config: Utf8PathBuf, } -fn main_debug_events(cli: CliDebugEvents) -> Result<()> { +fn main_debug_output(cli: CliDebugOutput) -> Result<()> { tracing_subscriber::fmt::init(); tracing::info!("Started tracing"); let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?; let config: Config = toml::from_str(&config).context("Failed to parse config file")?; + let data = read_data_from_disk(&config).context("Failed to read data from disk")?; + let tz = &config.output.timezone; let now = Utc::now().with_timezone(tz); - let data = read_calendars(&config).context("Failed to read data from disk")?; - - let events = process_data(&data, &config.output, now).context("Failed to process data")?; - - std::fs::create_dir_all("output")?; - output::atomic_write( - "output/calendars.html", - &output::calendars_page(&config.upstream_calendars(), now), - )?; - output::atomic_write( - "output/index.html", - &output::index_page(&config.output, &events, now), - )?; + let instances = process_data(&data, &config.output, now).context("Failed to process data")?; + output::write_html(&config.output, &config.upstreams(), &instances, now) + .context("Failed to output HTML")?; Ok(()) } #[derive(clap::Parser)] -struct CliDebugFeed { - #[arg(long)] - config: Utf8PathBuf, +struct CliDebugRss { + paths: Vec, } /// Wraps rss::Item in our own type suitable for merging @@ -335,11 +314,12 @@ pub(crate) struct FeedItem { inner: rss::Item, } -fn read_feeds(config: &Config) -> Result> { +fn main_debug_rss(cli: CliDebugRss) -> Result<()> { let mut items = Vec::new(); + let now = Utc::now(); - for feed in &config.feeds { - let s = std::fs::read(&feed.file_path)?; + for path in &cli.paths { + let s = std::fs::read(path)?; let channel = rss::Channel::read_from(std::io::BufReader::new(std::io::Cursor::new(s)))?; let channel_title = channel.title.clone(); @@ -360,65 +340,30 @@ fn read_feeds(config: &Config) -> Result> { } } - items.sort_by_key(|item| std::cmp::Reverse(item.date)); - Ok(items) -} + items.sort_by_key(|item| item.date); -fn main_debug_feed(cli: CliDebugFeed) -> Result<()> { - tracing_subscriber::fmt::init(); - tracing::info!("Started tracing"); - let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?; - let config: Config = toml::from_str(&config).context("Failed to parse config file")?; - - let tz = &config.output.timezone; - let now = Utc::now().with_timezone(tz); - let items = read_feeds(&config)?; + for item in items.iter().rev() { + println!("{}", item.channel_title); + println!("{}", item.inner.title.as_ref().unwrap()); + println!("{}", item.date.to_rfc3339()); + println!("{}", item.inner.link.as_ref().unwrap()); + println!(); + } std::fs::create_dir_all("output")?; - output::atomic_write("output/feed.html", &output::feed_page(&items, now))?; - - Ok(()) -} - -#[derive(clap::Parser)] -struct CliDebugOutput { - #[arg(long)] - config: Utf8PathBuf, -} - -fn main_debug_output(cli: CliDebugOutput) -> Result<()> { - tracing_subscriber::fmt::init(); - tracing::info!("Started tracing"); - let config = std::fs::read_to_string(&cli.config).context("Failed to read config file")?; - let config: Config = toml::from_str(&config).context("Failed to parse config file")?; - - let tz = &config.output.timezone; - let now = Utc::now().with_timezone(tz); - - let events = { - let data = read_calendars(&config).context("Failed to read calendars from disk")?; - process_data(&data, &config.output, now).context("Failed to process data")? - }; - - let feed_items = read_feeds(&config)?; - - std::fs::create_dir_all("output")?; - output::write_html( - &config.output, - &feed_items, - &config.upstream_calendars(), - &events, - now, + output::atomic_write( + "output/feed.html", + &output::feed_page(&items, now.with_timezone(&chrono_tz::UTC)), )?; + Ok(()) } #[derive(clap::Subcommand)] enum Commands { Auto(CliAuto), - DebugEvents(CliDebugEvents), - DebugFeed(CliDebugFeed), DebugOutput(CliDebugOutput), + DebugRss(CliDebugRss), } #[derive(clap::Parser)] @@ -433,8 +378,7 @@ fn main() -> Result<()> { match cli.command { Commands::Auto(x) => main_auto(x), - Commands::DebugEvents(x) => main_debug_events(x), - Commands::DebugFeed(x) => main_debug_feed(x), Commands::DebugOutput(x) => main_debug_output(x), + Commands::DebugRss(x) => main_debug_rss(x), } } diff --git a/src/output.rs b/src/output.rs index b861300..61fb772 100644 --- a/src/output.rs +++ b/src/output.rs @@ -74,10 +74,7 @@ fn calendar_link(calendar_ui: &crate::CalendarUi) -> maud::PreEscaped { } } -pub(crate) fn calendars_page( - upstreams: &[crate::CalendarUi], - now: DateTime, -) -> String { +fn calendars_page(upstreams: &[crate::CalendarUi], now: DateTime) -> String { let description = "A list of upstream calendars used by this Wide-Angle Calendar instance"; let title = "Upstream calendars"; @@ -101,13 +98,13 @@ pub(crate) fn calendars_page( } body { h1 { (title) } - p { "Written at: " (now.format("%F %T")) } p { a href="index.html" { "Wide-Angle Calendar" } " / " a href="calendars.html" { (title) } } + p { "Written at: " (now.format("%F %T")) } p { "These are the calendars that Wide-Angle Calendar pulls from." } ol { @@ -121,103 +118,11 @@ pub(crate) fn calendars_page( .into_string() } -pub(crate) fn feed_page(feed_items: &[crate::FeedItem], now: DateTime) -> String { - let mut feed_items = feed_items.iter(); - let mut machine = date_machine::StateMachine::default(); - let mut html_list = vec![]; - - loop { - match machine.pull() { - date_machine::Output::Data(date_machine::DataChunk { month, date, items }) => { - if let Some(month) = month { - html_list.push(maud::html! { h2 { (month) } }); - } - html_list.push(maud::html! { - (day_link(date)) - hr{} - ul { - @for item in items { - li { - (item) - } - } - } - }); - } - date_machine::Output::EndOfInput => break, - date_machine::Output::NeedInput => {} - } - - if let Some(item) = feed_items.next() { - let desc = item - .inner - .description - .as_ref() - .map(|desc| match desc.get(0..100) { - None => desc.to_string(), - Some(short_desc) => format!("{short_desc} ..."), - }); - - machine.push( - item.date.date_naive(), - maud::html! { - @if let Some(link) = &item.inner.link { - a href = (link) { (item.inner.title.as_deref().unwrap_or_default()) } - } @else { - (item.inner.title.as_deref().unwrap_or_default()) - } - ul { - @if let Some(desc) = desc { - li { (desc) } - } - li { (item.channel_title) } - } - }, - ); - } else { - machine.flush(); - } - } - - let description = "A feed merged from several input feeds"; - let title = "Feed"; - - maud::html! { - (maud::PreEscaped("")) - html lang="en" { - head { - meta http-equiv="Content-Type" content="text/html; charset=utf-8" {} - meta name="viewport" content="width=device-width, initial-scale=1" {} - (maud::PreEscaped(CSS)) - - meta property="og:locale" content="en" {} - meta property="og:type" content="website" {} - - meta name="description" content=(description) {} - meta property="description" content=(description) {} - meta property="og:description" content=(description) {} - - title { (title) } - met property="og:title" content=(title) {} - } - body { - h1 { (title) } - p { "Written at: " (now.format("%F %T")) " (But it may lag by several hours - Not for ICE spotting)" } - p { - a href="index.html" { "Wide-Angle Calendar" } - " / " - a href="feed.html" { (title) } - } - @for entry in html_list { - (entry) - } - } - } - } - .into_string() +pub(crate) fn feed_page(_feed_items: &[crate::FeedItem], _now: DateTime) -> String { + todo!() } -pub(crate) fn index_page( +fn index_page( config: &Config, instances: &[EventInstance], now: DateTime, @@ -343,13 +248,7 @@ pub(crate) fn index_page( h1 { (title) } img src="hero.webp" width="700" height="233" {} p { "Written at: " (now.format("%F %T")) } - p { - "Sub-pages:" - ul { - li { a href = "calendars.html" { "Upstream calendars" } } - li { a href = "feed.html" { "Feed" } } - } - } + p { a href = "calendars.html" { "Upstream calendars" } } @for entry in html_list { (entry) } @@ -368,224 +267,16 @@ pub(crate) fn atomic_write(path: &str, content: &str) -> Result<()> { pub(crate) fn write_html( config: &Config, - feed_items: &[crate::FeedItem], - upstream_calendars: &[crate::CalendarUi], - events: &[EventInstance], + // feed_items: &[crate::FeedItem], + upstreams: &[crate::CalendarUi], + instances: &[EventInstance], now: DateTime, ) -> Result<()> { std::fs::create_dir_all("output")?; - atomic_write( - "output/calendars.html", - &calendars_page(upstream_calendars, now), - )?; - atomic_write("output/feed.html", &feed_page(feed_items, now))?; - atomic_write("output/index.html", &index_page(config, events, now))?; + atomic_write("output/calendars.html", &calendars_page(upstreams, now))?; + // atomic_write("output/feed.html", &feed_page(feed_items, now)?)?; + atomic_write("output/index.html", &index_page(config, instances, now))?; Ok(()) } - -/// Takes in a stream of items with dates, and formats them out categorized by month and date. -mod date_machine { - use chrono::NaiveDate; - use std::{cmp::PartialEq, collections::VecDeque, fmt}; - - pub(crate) struct DataChunk { - pub(crate) month: Option, - pub(crate) date: NaiveDate, - pub(crate) items: Vec, - } - - // Debug only if T: Debug - impl fmt::Debug for DataChunk { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DataChunk") - .field("month", &self.month) - .field("date", &self.date) - .field("items", &self.items) - .finish() - } - } - - // PartialEq only if T: PartialEq - impl PartialEq for DataChunk { - fn eq(&self, other: &Self) -> bool { - self.month == other.month && self.date == other.date && self.items == other.items - } - } - - pub(crate) enum Output { - Data(DataChunk), - EndOfInput, - NeedInput, - } - - // Debug only if T: Debug - impl fmt::Debug for Output { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Output::Data(chunk) => f.debug_tuple("Data").field(chunk).finish(), - Output::EndOfInput => f.write_str("EndOfInput"), - Output::NeedInput => f.write_str("NeedInput"), - } - } - } - - // PartialEq only if T: PartialEq - impl PartialEq for Output { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Output::Data(a), Output::Data(b)) => a == b, - (Output::EndOfInput, Output::EndOfInput) => true, - (Output::NeedInput, Output::NeedInput) => true, - _ => false, - } - } - } - - #[derive(Default)] - pub(crate) struct StateMachine { - data: VecDeque>, - flushed: bool, - last_month_printed: Option, - } - - impl StateMachine { - pub(crate) fn pull(&mut self) -> Output { - if self.data.len() >= 2 - && let Some(chunk) = self.data.pop_front() - { - if chunk.month.is_some() && self.last_month_printed != chunk.month { - self.last_month_printed = chunk.month.clone(); - } - return Output::Data(chunk); - } - if self.flushed { - if let Some(chunk) = self.data.pop_front() { - return Output::Data(chunk); - } - return Output::EndOfInput; - } - Output::NeedInput - } - - pub(crate) fn push(&mut self, date: NaiveDate, item: T) { - let month = Some(date.format("%B").to_string()); - let month = if month == self.last_month_printed { - None - } else { - month - }; - if let Some(chunk) = self.data.front_mut() { - if chunk.date == date { - chunk.items.push(item); - } else if chunk.month == month { - self.data.push_back(DataChunk { - month: None, - date, - items: vec![item], - }); - } else { - self.data.push_back(DataChunk { - month, - date, - items: vec![item], - }); - } - } else { - self.data.push_back(DataChunk { - month, - date, - items: vec![item], - }); - } - } - - pub(crate) fn flush(&mut self) { - self.flushed = true; - } - } -} - -#[cfg(test)] -mod tests { - use chrono::NaiveDate; - - #[test] - fn date_state_machine() { - use super::date_machine::*; - - let mut machine: StateMachine<&str> = StateMachine::default(); - let mut items = vec![ - (2025, 09, 10, "alpha"), - (2025, 09, 10, "bravo"), - (2025, 09, 11, "charlie"), - (2025, 09, 12, "delta"), - (2025, 09, 13, "echo"), - (2025, 10, 1, "foxtrot"), - (2025, 10, 2, "golf"), - (2025, 10, 3, "hotel"), - ] - .into_iter(); - let mut output = vec![]; - - loop { - match machine.pull() { - Output::Data(chunk) => { - output.push(chunk); - continue; - } - Output::EndOfInput => break, - Output::NeedInput => {} - } - - if let Some((y, m, d, item)) = items.next() { - let date = NaiveDate::from_ymd_opt(y, m, d).unwrap(); - machine.push(date, item); - } else { - machine.flush(); - } - } - - let expected = vec![ - DataChunk { - month: Some("September".to_string()), - date: NaiveDate::from_ymd_opt(2025, 09, 10).unwrap(), - items: vec!["alpha", "bravo"], - }, - DataChunk { - month: None, - date: NaiveDate::from_ymd_opt(2025, 09, 11).unwrap(), - items: vec!["charlie"], - }, - DataChunk { - month: None, - date: NaiveDate::from_ymd_opt(2025, 09, 12).unwrap(), - items: vec!["delta"], - }, - DataChunk { - month: None, - date: NaiveDate::from_ymd_opt(2025, 09, 13).unwrap(), - items: vec!["echo"], - }, - DataChunk { - month: Some("October".to_string()), - date: NaiveDate::from_ymd_opt(2025, 10, 1).unwrap(), - items: vec!["foxtrot"], - }, - DataChunk { - month: None, - date: NaiveDate::from_ymd_opt(2025, 10, 2).unwrap(), - items: vec!["golf"], - }, - DataChunk { - month: None, - date: NaiveDate::from_ymd_opt(2025, 10, 3).unwrap(), - items: vec!["hotel"], - }, - ]; - assert_eq!(output, expected, "{output:#?} != {expected:#?}"); - - assert_eq!("abc".get(0..100).unwrap_or("abc"), "abc"); - } -}