Compare commits

...

6 Commits
master ... cli

Author SHA1 Message Date
blank X e06eaf61ba
Update dependencies 2022-04-30 17:54:44 +07:00
blank X 9df5c17537 Use quick-xml instead of rss 2021-01-09 01:09:24 +07:00
blank X 7732bce38b Only use async for getting data from Anilist 2021-01-07 11:39:25 +07:00
blank X adc02b3d03 Update to tokio 1.0 2021-01-07 11:32:40 +07:00
blank X 5561123d6b Update copyright year from 2020 to 2021 2021-01-01 10:44:42 +07:00
blank X 5d22ead7d7 Make it a CLI program 2020-12-03 13:25:07 +07:00
5 changed files with 400 additions and 1036 deletions

1198
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rss-anime-notifier-rs" name = "rss-anime-notifier-rs"
version = "0.1.4" version = "0.2.4"
authors = ["blank X <theblankx@protonmail.com>"] authors = ["blank X <theblankx@protonmail.com>"]
edition = "2018" edition = "2018"
@ -10,10 +10,9 @@ edition = "2018"
lto = true lto = true
[dependencies] [dependencies]
rss = "1.9.0" quick-xml = "0.22"
warp = "0.2.5" chrono = "0.4"
chrono = "0.4.19"
serde_json = "1.0" serde_json = "1.0"
reqwest = "0.10.8" reqwest = "0.11"
serde = { version = "1.0.117", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tokio = { version = "0.2.22", features = ["rt-core"] } tokio = { version = "1.18", features = ["rt"] }

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 blank X Copyright (c) 2021 blank X
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,16 +1,11 @@
# RSS Anime Notifier but in Rust # RSS Anime Notifier but in Rust
Get anime schedule from https://anichart.net and gives an RSS feed to episodes that have already aired Get anime schedule from https://anichart.net and gives an RSS feed to episodes that have already aired to standard output
### How to Build and Run ### How to Build and Run
https://doc.rust-lang.org/stable/book/ch01-03-hello-cargo.html#building-and-running-a-cargo-project https://doc.rust-lang.org/stable/book/ch01-03-hello-cargo.html#building-and-running-a-cargo-project
### Environment Variables
`PORT` - Specify the port to listen to, default: 8080
### How to Use ### How to Use
1. Start the script, obviously `rss-anime-notifier-rs <anime id>`
2. Get the Anilist anime ID For example, if the URL is https://anilist.co/anime/112609/Majo-no-Tabitabi, the anime ID is 112609
For example, if the URL is https://anilist.co/anime/112609/Majo-no-Tabitabi, the anime ID is 112609 To use with newsboat, check out https://brokenco.de/2020/07/07/newsboat-wacky-feeds.html
3. Add http://127.0.0.1:[port]/[animeID] to your RSS reader
For example, if your port is 8080 and the anime ID is 112609, you'd use http://127.0.0.1:8080/112609

View File

@ -1,18 +1,18 @@
extern crate rss;
extern crate warp;
extern crate serde; extern crate serde;
extern crate tokio; extern crate tokio;
extern crate chrono; extern crate chrono;
extern crate reqwest; extern crate reqwest;
extern crate serde_json; extern crate serde_json;
use std::env; use std::env;
use std::convert::Infallible; use std::path::Path;
use std::io::Cursor;
use std::process::exit;
use reqwest::Client; use reqwest::Client;
use serde_json::json; use serde_json::json;
use quick_xml::Writer;
use serde::Deserialize; use serde::Deserialize;
use warp::{Filter, http::response};
use chrono::{Utc, DateTime, NaiveDateTime}; use chrono::{Utc, DateTime, NaiveDateTime};
use rss::{Item, ItemBuilder, ChannelBuilder, GuidBuilder}; use quick_xml::events::{Event, BytesStart, BytesText, BytesEnd};
const QUERY: &str = r#"query ($id: Int) { const QUERY: &str = r#"query ($id: Int) {
Media (id: $id) { Media (id: $id) {
@ -49,7 +49,7 @@ struct Data {
struct Media { struct Media {
pub title: MediaTitle, pub title: MediaTitle,
pub airing_schedule: AiringScheduleConnection, pub airing_schedule: AiringScheduleConnection,
pub episodes: Option<isize>, pub episodes: Option<i32>,
pub site_url: String pub site_url: String
} }
@ -67,43 +67,48 @@ struct AiringScheduleConnection {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all="camelCase")] #[serde(rename_all="camelCase")]
struct AiringSchedule { struct AiringSchedule {
pub id: isize, pub id: i32,
pub airing_at: i64, pub airing_at: i64,
pub time_until_airing: i64, pub time_until_airing: i64,
pub episode: isize pub episode: i32
} }
async fn give_response(anime_id: usize, client: Client) -> Result<impl warp::Reply, Infallible> { async fn get_data(anime_id: i32) -> Root {
let client = Client::new();
let json = json!({"query": QUERY, "variables": {"id": anime_id}}); let json = json!({"query": QUERY, "variables": {"id": anime_id}});
let resp = match client.post("https://graphql.anilist.co/") serde_json::from_str(&client.post("https://graphql.anilist.co/")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Accept", "application/json") .header("Accept", "application/json")
.body(json.to_string()) .body(json.to_string())
.send() .send()
.await { .await
Ok(resp) => match resp.text().await { .unwrap()
Ok(resp) => match serde_json::from_str::<Root>(&resp) { .text()
Ok(resp) => resp, .await
Err(err) => { .unwrap()).unwrap()
eprintln!("ERROR: {}", err); }
return Ok(response::Builder::new().status(502).header("Content-Type", "type/plain").body(format!("502\n{}", err)));
} fn main() {
}, let mut args = env::args();
Err(err) => { let path = args.next().expect("Cannot get binary path");
eprintln!("ERROR: {}", err); let path = Path::new(&path).file_stem().unwrap().to_str().unwrap();
return Ok(response::Builder::new().status(502).header("Content-Type", "type/plain").body(format!("502\n{}", err))); let anime_id = match args.next() {
} Some(anime_id) => anime_id.parse::<i32>().unwrap(),
}, None => {
Err(err) => { eprintln!("Usage: {} <anime id>", path);
eprintln!("ERROR: {}", err); exit(1);
return Ok(response::Builder::new().status(503).header("Content-Type", "type/plain").body(format!("503\n{}", err))); }
} };
}; let resp = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(get_data(anime_id));
let mut pub_date = Utc::now().to_rfc2822(); let mut pub_date = Utc::now().to_rfc2822();
let mut last_build_date = pub_date.clone(); let mut last_build_date = pub_date.clone();
let resp = &resp.data.media; let resp = &resp.data.media;
let title = resp.title.english.as_ref().unwrap_or(&resp.title.romaji); let title = resp.title.english.as_ref().unwrap_or(&resp.title.romaji);
let mut items: Vec<Item> = Vec::with_capacity(resp.airing_schedule.nodes.len()); let mut items: Vec<(i32, String, String)> = Vec::with_capacity(resp.airing_schedule.nodes.len());
for i in &resp.airing_schedule.nodes { for i in &resp.airing_schedule.nodes {
let i_date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(i.airing_at, 0), Utc).to_rfc2822(); let i_date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(i.airing_at, 0), Utc).to_rfc2822();
pub_date = i_date.clone(); pub_date = i_date.clone();
@ -115,49 +120,114 @@ async fn give_response(anime_id: usize, client: Client) -> Result<impl warp::Rep
if Some(i.episode) == resp.episodes { if Some(i.episode) == resp.episodes {
title.push_str(" END"); title.push_str(" END");
} }
items.push(ItemBuilder::default() items.push((i.id, i_date, title));
.title(title)
.link(resp.site_url.clone())
.pub_date(i_date)
.guid(GuidBuilder::default().permalink(false).value(format!("ran-{}-{}", anime_id, i.id)).build().unwrap())
.build()
.unwrap()
);
} }
items.reverse(); items.reverse();
let channel = ChannelBuilder::default() let mut writer = Writer::new(Cursor::new(Vec::new()));
.title(title)
.link(resp.site_url.clone())
.description(format!("Aired episodes of {}", title))
.pub_date(pub_date)
.last_build_date(last_build_date)
.items(items)
.build()
.unwrap();
eprintln!("INFO: {} ({}) requested", anime_id, title);
Ok(response::Builder::new()
.status(200)
.header("Content-Type", "application/rss+xml")
.body(channel.to_string()))
}
async fn async_main() { {
let port = match env::var("PORT") { let mut elem = BytesStart::owned(b"rss".to_vec(), 3);
Ok(port) => port.parse::<u16>().unwrap(), elem.push_attribute(("version", "2.0"));
Err(_) => 8080 writer.write_event(Event::Start(elem)).unwrap();
};
let client = Client::new();
let client = warp::any().map(move || client.clone());
let log = warp::log("rss-anime-notifier-rs");
let bare_path = warp::path!(usize).and(client).and_then(give_response).with(log);
let routes = warp::get().and(bare_path); let elem = BytesStart::owned(b"channel".to_vec(), 7);
eprintln!("INFO: Listening on 127.0.0.1:{}", port); writer.write_event(Event::Start(elem)).unwrap();
warp::serve(routes).run(([127, 0, 0, 1], port)).await;
}
fn main() { let elem = BytesStart::owned(b"title".to_vec(), 5);
tokio::runtime::Runtime::new() writer.write_event(Event::Start(elem)).unwrap();
.unwrap()
.block_on(async_main()); let elem = BytesText::from_plain_str(&title).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"title".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"link".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&resp.site_url).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"link".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"description".to_vec(), 11);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&format!("Aired episodes of {}", &title)).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"description".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"pubDate".to_vec(), 7);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&pub_date).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"pubDate".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"lastBuildDate".to_vec(), 13);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&last_build_date).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"lastBuildDate".to_vec());
writer.write_event(Event::End(elem)).unwrap();
}
for (id, date, title) in items {
let elem = BytesStart::owned(b"item".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesStart::owned(b"title".to_vec(), 5);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&title).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"title".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"link".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&resp.site_url).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"link".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"guid".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&format!("ran-{}-{}", &anime_id, id)).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"guid".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"pubDate".to_vec(), 7);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&date).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"pubDate".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesEnd::owned(b"item".to_vec());
writer.write_event(Event::End(elem)).unwrap();
}
let elem = BytesEnd::owned(b"channel".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesEnd::owned(b"rss".to_vec());
writer.write_event(Event::End(elem)).unwrap();
println!("{}", String::from_utf8(writer.into_inner().into_inner()).unwrap());
} }