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

234 lines
7.3 KiB
Rust

extern crate serde;
extern crate tokio;
extern crate chrono;
extern crate reqwest;
extern crate serde_json;
use std::env;
use std::path::Path;
use std::io::Cursor;
use std::process::exit;
use reqwest::Client;
use serde_json::json;
use quick_xml::Writer;
use serde::Deserialize;
use chrono::{Utc, DateTime, NaiveDateTime};
use quick_xml::events::{Event, BytesStart, BytesText, BytesEnd};
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<i32>,
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: i32,
pub airing_at: i64,
pub time_until_airing: i64,
pub episode: i32
}
async fn get_data(anime_id: i32) -> Root {
let client = Client::new();
let json = json!({"query": QUERY, "variables": {"id": anime_id}});
serde_json::from_str(&client.post("https://graphql.anilist.co/")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(json.to_string())
.send()
.await
.unwrap()
.text()
.await
.unwrap()).unwrap()
}
fn main() {
let mut args = env::args();
let path = args.next().expect("Cannot get binary path");
let path = Path::new(&path).file_stem().unwrap().to_str().unwrap();
let anime_id = match args.next() {
Some(anime_id) => anime_id.parse::<i32>().unwrap(),
None => {
eprintln!("Usage: {} <anime id>", path);
exit(1);
}
};
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 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<(i32, String, String)> = 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((i.id, i_date, title));
}
items.reverse();
let mut writer = Writer::new(Cursor::new(Vec::new()));
{
let mut elem = BytesStart::owned(b"rss".to_vec(), 3);
elem.push_attribute(("version", "2.0"));
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesStart::owned(b"channel".to_vec(), 7);
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"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());
}