rss-anime-notifier-rs/src/main.rs

115 lines
4.0 KiB
Rust

extern crate rss;
extern crate warp;
extern crate tokio;
extern crate chrono;
extern crate reqwest;
extern crate serde_json;
use std::env;
use std::convert::Infallible;
use reqwest::Client;
use serde_json::json;
use warp::{Filter, http::response};
use chrono::{Utc, DateTime, NaiveDateTime};
use rss::{Item, ItemBuilder, ChannelBuilder, GuidBuilder};
const QUERY: &str = r#"query ($id: Int) {
Media (id: $id) {
title {
romaji
english
}
airingSchedule {
nodes {
id
airingAt
timeUntilAiring
episode
}
}
episodes
siteUrl
}
}"#;
async fn give_response(anime_id: usize) -> Result<impl warp::Reply, Infallible> {
let json = json!({"query": QUERY, "variables": {"id": anime_id}});
let client = Client::new();
let resp = match client.post("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(json.to_string())
.send()
.await {
Ok(resp) => match resp.text().await {
Ok(resp) => match serde_json::from_str::<serde_json::Value>(&resp) {
Ok(resp) => resp,
Err(err) => {
eprintln!("ERROR: {}", err);
return Ok(response::Builder::new().status(502).header("Content-Type", "type/plain").body(format!("502\n{}", err)));
}
},
Err(err) => {
eprintln!("ERROR: {}", err);
return Ok(response::Builder::new().status(502).header("Content-Type", "type/plain").body(format!("502\n{}", err)));
}
},
Err(err) => {
eprintln!("ERROR: {}", err);
return Ok(response::Builder::new().status(503).header("Content-Type", "type/plain").body(format!("503\n{}", err)));
}
};
let resp = &resp["data"]["Media"];
let title = resp["title"]["english"].as_str().unwrap_or(resp["title"]["romaji"].as_str().unwrap());
let mut items: Vec<Item> = Vec::new();
let mut pub_date = Utc::now().to_rfc2822();
let mut last_build_date = pub_date.clone();
for i in resp["airingSchedule"]["nodes"].as_array().unwrap() {
let i_date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(i["airingAt"].as_i64().unwrap(), 0), Utc).to_rfc2822();
pub_date = i_date.clone();
if i["timeUntilAiring"].as_i64().unwrap() > 0 {
break;
}
last_build_date = i_date.clone();
let mut title = format!("{} - {}", title, i["episode"].as_u64().unwrap());
if i["episode"].as_u64().unwrap() == resp["episodes"].as_u64().unwrap_or(0) {
title.push_str(" END");
}
items.push(ItemBuilder::default()
.title(title)
.link(resp["siteUrl"].as_str().unwrap().to_string())
.pub_date(i_date)
.guid(GuidBuilder::default().permalink(false).value(format!("ran-{}-{}", anime_id, i["id"].as_u64().unwrap())).build().unwrap())
.build()
.unwrap()
);
}
items.reverse();
let channel = ChannelBuilder::default()
.title(title)
.link(resp["siteUrl"].as_str().unwrap())
.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()))
}
#[tokio::main]
async fn main() {
let port = match env::var("PORT") {
Ok(port) => port.parse::<u16>().unwrap(),
Err(_) => 8080
};
let bare_path = warp::path!(usize).and_then(give_response);
let routes = warp::get().and(bare_path);
eprintln!("INFO: Listening on 127.0.0.1:{}", port);
warp::serve(routes).run(([127, 0, 0, 1], port)).await;
}