use anyhow::{ bail, Context, Result, }; use clap::{ Parser, }; use feed_rs::{ parser::parse, model::{ Entry, Feed, }, }; use reqwest::{ IntoUrl, StatusCode, Url, }; #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { /// Path to file containing feeds #[arg(long, default_value_t = String::from("feeds.txt"))] feeds: String, /// Path to output file #[arg(long, default_value_t = String::from("index.html"))] out: String, } struct FeedItem { link: String, title: String, time: chrono::DateTime, } fn fetch_feed(url: impl IntoUrl) -> Result { let r = reqwest::blocking::get(url) .context("Failed to fetch feed")?; if r.status() != StatusCode::OK { bail!("Fetched feed returned unuseable status code {}", r.status()); } let content = r.bytes() .context("Failed to read feed contents")?; let feed = parse(&content[..]) .context("Failed to parse feed")?; Ok(feed) } struct FeedFile { name: String, urls: Vec, } fn read_feed_file(path: &str) -> Result { let mut name = String::from(path); let mut urls = Vec::new(); for line in std::fs::read_to_string(path)?.lines() { let line = line.trim().to_string(); if line == "" { continue; } if line.starts_with("# name: ") { name = String::from(&line[8..]); continue; } if line.starts_with("#") { continue; } let url = Url::parse(&line)?; urls.push(url); } Ok(FeedFile{ name: name, urls: urls, }) } fn make_feed_item(entry: Entry) -> Result { Ok(FeedItem { link: entry.links .first() .context("Unable to retrieve link from feed entry")? .href .clone(), title: entry.title .context("Unable to retrieve title from feed entry")? .content .clone(), time: match entry.published { Some(time) => time, None => entry.updated .context("Unable to retrieve publishing time from feed entry")?, }, }) } fn main() -> Result<()> { let cli = Cli::parse(); let feed_file = read_feed_file(&cli.feeds)?; let mut items: Vec = Vec::new(); for feed_url in feed_file.urls { println!("Loading feed {}", feed_url); let feed_entries = match fetch_feed(feed_url) { Ok(feed) => feed.entries, Err(e) => { println!("\t{}, skipping", e); continue; }, }; for entry in feed_entries { match make_feed_item(entry) { Ok(item) => { items.push(item); }, Err(e) => { println!("\t{}, skipping", e); }, } } } items.sort_by(|a, b| a.time.cmp(&b.time).reverse()); let mut out = String::new(); out.push_str("\n"); out.push_str("\n"); out.push_str("\n"); out.push_str(&format!("{} - rainbowrss", feed_file.name)); out.push_str("\n"); out.push_str("\n"); out.push_str(&format!("

{}

", feed_file.name)); out.push_str("
    \n"); for item in items { out.push_str(&format!("
  • [l] {} {}
  • \n", item.link, item.title, item.time)); } out.push_str("
\n"); out.push_str(&format!("Last updated: {}", chrono::Utc::now())); out.push_str("\n"); out.push_str("\n"); std::fs::write(&cli.out, out) .context("Failed to write output to file")?; Ok(()) }