164 lines
4.8 KiB
Rust
164 lines
4.8 KiB
Rust
extern crate rss;
|
|
extern crate warp;
|
|
extern crate serde;
|
|
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 serde::Deserialize;
|
|
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
|
|
}
|
|
}"#;
|
|
|
|
#[derive(Deserialize)]
|
|
struct Root {
|
|
pub data: Data
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all="PascalCase")]
|
|
struct Data {
|
|
pub media: Media
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all="camelCase")]
|
|
struct Media {
|
|
pub title: MediaTitle,
|
|
pub airing_schedule: AiringScheduleConnection,
|
|
pub episodes: Option<isize>,
|
|
pub site_url: String
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct MediaTitle {
|
|
pub romaji: String,
|
|
pub english: Option<String>
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct AiringScheduleConnection {
|
|
pub nodes: Vec<AiringSchedule>
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all="camelCase")]
|
|
struct AiringSchedule {
|
|
pub id: isize,
|
|
pub airing_at: i64,
|
|
pub time_until_airing: i64,
|
|
pub episode: isize
|
|
}
|
|
|
|
async fn give_response(anime_id: usize, client: Client) -> Result<impl warp::Reply, Infallible> {
|
|
let json = json!({"query": QUERY, "variables": {"id": anime_id}});
|
|
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::<Root>(&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 mut pub_date = Utc::now().to_rfc2822();
|
|
let mut last_build_date = pub_date.clone();
|
|
let resp = &resp.data.media;
|
|
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());
|
|
for i in &resp.airing_schedule.nodes {
|
|
let i_date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(i.airing_at, 0), Utc).to_rfc2822();
|
|
pub_date = i_date.clone();
|
|
if i.time_until_airing > 0 {
|
|
break;
|
|
}
|
|
last_build_date = i_date.clone();
|
|
let mut title = format!("{} - {}", title, i.episode);
|
|
if Some(i.episode) == resp.episodes {
|
|
title.push_str(" END");
|
|
}
|
|
items.push(ItemBuilder::default()
|
|
.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();
|
|
let channel = ChannelBuilder::default()
|
|
.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") {
|
|
Ok(port) => port.parse::<u16>().unwrap(),
|
|
Err(_) => 8080
|
|
};
|
|
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);
|
|
eprintln!("INFO: Listening on 127.0.0.1:{}", port);
|
|
warp::serve(routes).run(([127, 0, 0, 1], port)).await;
|
|
}
|
|
|
|
fn main() {
|
|
tokio::runtime::Runtime::new()
|
|
.unwrap()
|
|
.block_on(async_main());
|
|
}
|