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, pub site_url: String } #[derive(Deserialize)] struct MediaTitle { pub romaji: String, pub english: Option } #[derive(Deserialize)] struct AiringScheduleConnection { pub nodes: Vec } #[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 { 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::(&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 = Vec::with_capacity(resp.airing_schedule.nodes.len()); for i in &resp.airing_schedule.nodes { let i_date = DateTime::::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::().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()); }