initial commit

This commit is contained in:
yggverse 2025-06-14 04:20:32 +03:00
parent fc4f0d6d1c
commit 0506b4dcb2
11 changed files with 455 additions and 1 deletions

37
src/argument.rs Normal file
View file

@ -0,0 +1,37 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Argument {
/// Show output (`d` - debug, `e` - error, `i` - info)
#[arg(short, long, default_value_t = String::from("ei"))]
pub debug: String,
/// Export formats (`html`,`md`,etc.)
#[arg(short, long, default_values_t = [String::from("html")])]
pub format: Vec<String>,
/// Limit channel items (unlimited by default)
#[arg(short, long)]
pub limit: Option<usize>,
/// RSS feed URL(s)
#[arg(short, long)]
pub source: Vec<String>,
/// Destination directory
#[arg(long)]
pub target: Vec<String>,
/// Path to template directory
#[arg(long, default_value_t = String::from("template"))]
pub template: String,
/// Use custom time format
#[arg(long, default_value_t = String::from("%Y/%m/%d %H:%M:%S %z"))]
pub time_format: String,
/// Update timeout in seconds
#[arg(short, long, default_value_t = 60)]
pub update: u64,
}

48
src/debug.rs Normal file
View file

@ -0,0 +1,48 @@
use anyhow::{Result, bail};
#[derive(PartialEq)]
pub enum Level {
//Debug,
//Error,
Info,
}
impl Level {
fn parse(value: char) -> Result<Self> {
match value {
//'d' => Ok(Self::Debug),
//'e' => Ok(Self::Error),
'i' => Ok(Self::Info),
_ => bail!("Unsupported debug value `{value}`!"),
}
}
}
pub struct Debug(Vec<Level>);
impl Debug {
pub fn init(values: &str) -> Result<Self> {
let mut l = Vec::with_capacity(values.len());
for s in values.to_lowercase().chars() {
l.push(Level::parse(s)?);
}
Ok(Self(l))
}
/* @TODO
pub fn error(&self, message: &str) {
if self.has(Level::Error) {
eprintln!("[{}] [error] {message}", t());
}
} */
pub fn info(&self, message: &str) {
if self.0.contains(&Level::Info) {
println!("[{}] [info] {message}", t());
}
}
}
fn t() -> String {
crate::time::utc().to_rfc3339()
}

57
src/format.rs Normal file
View file

@ -0,0 +1,57 @@
use anyhow::{bail, Result};
pub struct Template {
pub index: String,
pub index_item: String,
}
impl Template {
pub fn html(template_path: &str) -> Result<Self> {
use std::{fs::read_to_string, path::PathBuf, str::FromStr};
let mut p = PathBuf::from_str(template_path)?;
p.push("html");
Ok(Self {
index: read_to_string(&{
let mut p = PathBuf::from(&p);
p.push("index.html");
p
})?,
index_item: read_to_string(&{
let mut p = PathBuf::from(&p);
p.push("index");
p.push("item.html");
p
})?,
})
}
}
pub enum Type {
Html(Template),
}
impl Type {
fn parse(format: &str, template_path: &str) -> Result<Self> {
if matches!(format.to_lowercase().as_str(), "html") {
return Ok(Self::Html(Template::html(template_path)?));
}
bail!("Format `{format}` support yet not implemented!")
}
}
pub struct Format(Vec<Type>);
impl Format {
pub fn init(values: &Vec<String>, template: &str) -> Result<Self> {
let mut f = Vec::with_capacity(values.len());
for s in values {
f.push(Type::parse(s, template)?);
}
Ok(Self(f))
}
pub fn get(&self) -> &Vec<Type> {
&self.0
}
}

101
src/main.rs Normal file
View file

@ -0,0 +1,101 @@
mod argument;
mod debug;
mod format;
mod target;
mod time;
use anyhow::{Result, bail};
use argument::Argument;
use debug::Debug;
use format::Format;
use format::Type;
use target::Target;
use time::Time;
fn main() -> Result<()> {
use clap::Parser;
use std::{thread::sleep, time::Duration};
let argument = Argument::parse();
// parse argument values once
let debug = Debug::init(&argument.debug)?;
let format = Format::init(&argument.format, &argument.template)?;
let target = Target::init(&argument.target)?;
let time = Time::init(argument.time_format);
// validate some targets
if argument.source.len() != argument.target.len() {
bail!("Targets quantity does not match sources!")
}
debug.info("Crawler started");
loop {
debug.info("Begin new crawl queue...");
for (i, s) in argument.source.iter().enumerate() {
debug.info(&format!("Update {s}..."));
crawl((s, i), &format, &target, &time, &argument.limit)?;
}
debug.info(&format!(
"Crawl queue completed, wait {} seconds to continue...",
argument.update
));
sleep(Duration::from_secs(argument.update));
}
}
fn crawl(
source: (&str, usize),
format: &Format,
target: &Target,
time: &Time,
limit: &Option<usize>,
) -> Result<()> {
use reqwest::blocking::get;
use rss::Channel;
use std::{fs::File, io::Write};
let c = Channel::read_from(&get(source.0)?.bytes()?[..])?;
let i = c.items();
let l = limit.unwrap_or(i.len());
for f in format.get() {
match f {
Type::Html(template) => File::create(target.index(source.1, "html"))?.write_all(
template
.index
.replace("{title}", c.title())
.replace("{description}", c.description())
.replace("{link}", c.link())
.replace("{language}", c.language().unwrap_or_default())
.replace("{pub_date}", &time.format(c.pub_date()))
.replace("{last_build_date}", &time.format(c.last_build_date()))
.replace("{time_generated}", &time.now())
.replace("{items}", &{
let mut items = String::with_capacity(l);
for (n, item) in i.iter().enumerate() {
if n > l {
break;
}
items.push_str(
&template
.index_item
.replace("{title}", item.title().unwrap_or_default())
.replace(
"{description}",
item.description().unwrap_or_default(),
)
.replace("{link}", item.link().unwrap_or_default())
.replace("{time}", &time.format(item.pub_date())),
)
}
items
})
.as_bytes(),
)?,
}
}
Ok(())
}

25
src/target.rs Normal file
View file

@ -0,0 +1,25 @@
use anyhow::{bail, Result};
use std::{fs, path::PathBuf, str::FromStr};
pub struct Target(Vec<PathBuf>);
impl Target {
pub fn init(paths: &Vec<String>) -> Result<Self> {
let mut t = Vec::with_capacity(paths.len());
for path in paths {
let p = PathBuf::from_str(path)?;
if fs::metadata(&p).is_ok_and(|t| t.is_file()) {
bail!("Target destination exists and not directory!")
}
fs::create_dir_all(&p)?;
t.push(p)
}
Ok(Self(t))
}
pub fn index(&self, index: usize, extension: &str) -> PathBuf {
let mut p = PathBuf::from(&self.0[index]);
p.push(format!("index.{extension}"));
p
}
}

29
src/time.rs Normal file
View file

@ -0,0 +1,29 @@
use chrono::{DateTime, Utc};
pub struct Time(String);
impl Time {
pub fn init(format: String) -> Self {
Self(format)
}
pub fn format(&self, value: Option<&str>) -> String {
match value {
Some(v) => chrono::DateTime::parse_from_rfc2822(v)
.unwrap()
.format(&self.0)
.to_string(),
None => todo!(),
}
}
pub fn now(&self) -> String {
utc().format(&self.0).to_string()
}
}
pub fn utc() -> DateTime<Utc> {
let s = std::time::SystemTime::now();
let c: chrono::DateTime<chrono::Utc> = s.into();
c
}