use clap::Parser;
use feed_rs::parser;
use serde::{Deserialize, Serialize};
use std::{
fs::{read_to_string, write},
path::PathBuf,
thread::sleep,
time::Duration,
};
use tera::{Context, Tera};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
config: Option<PathBuf>,
}
#[derive(Deserialize)]
struct Config {
name: String,
feeds: Vec<String>,
interval: Option<u64>,
template: Option<String>,
output: Option<String>,
}
#[derive(Serialize)]
struct Entry {
title: String,
url: String,
feed: String,
published: String,
summary: Option<String>,
authors: String,
}
#[derive(Serialize)]
struct Data {
name: String,
entries: Vec<Entry>,
}
fn build(config: &Config) -> anyhow::Result<String> {
let mut entries: Vec<Entry> = vec![];
for url in &config.feeds {
let text = reqwest::blocking::get(url)?.text()?;
let feed = parser::parse(text.as_bytes()).unwrap();
let feed_title = {
if feed.title.is_some() {
feed.title.unwrap().content
} else {
url.to_string()
}
};
for entry in feed.entries {
let mut index = 0;
if entry.title.is_none() || entry.published.is_none() {
continue;
}
for e in &entries {
if e.published < entry.published.unwrap_or_default().to_rfc3339() {
break;
} else {
index += 1;
}
}
let summary = {
if entry.summary.is_some() {
Some(entry.summary.unwrap().content)
} else {
None
}
};
let mut authors = vec![];
for author in entry.authors {
authors.push(author.name);
}
entries.insert(
index,
Entry {
title: entry.title.unwrap().content,
url: entry.links[0].clone().href,
feed: feed_title.clone(),
published: entry
.published
.unwrap()
.to_rfc3339()
.split_once('+')
.unwrap()
.0
.to_string(),
authors: authors.join(", "),
summary,
},
);
}
}
let data = Data {
name: config.name.clone(),
entries,
};
let template: String;
if let Some(f) = &config.template {
template = read_to_string(f).expect("Cannot read template file :(");
} else {
template = include_str!("default.html").to_string();
}
Ok(Tera::one_off(
&template,
&Context::from_serialize(data)?,
true,
)?)
}
fn main() {
let args = Cli::parse();
let config: Config = toml::from_str(
&read_to_string(args.config.unwrap_or(PathBuf::from("agro.toml")))
.expect("Cannot read config file :("),
)
.expect("Invalid config!");
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]);
loop {
println!("Fetching...");
let html = build(&config);
if let Err(err) = html {
println!("{}", err);
continue;
}
write(
config.output.as_ref().unwrap_or(&"out.html".to_string()),
html.unwrap(),
)
.expect("Could not write to output");
sleep(Duration::from_secs(60 * config.interval.unwrap_or(60)));
}
}