Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
blank X | e06eaf61ba | |
blank X | 9df5c17537 | |
blank X | 7732bce38b | |
blank X | adc02b3d03 | |
blank X | 5561123d6b | |
blank X | 5d22ead7d7 |
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
|
@ -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"] }
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||||
|
|
13
README.md
13
README.md
|
@ -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
|
|
||||||
|
|
210
src/main.rs
210
src/main.rs
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue