initial commit
Dieser Commit ist enthalten in:
Commit
5e376de102
10 geänderte Dateien mit 3719 neuen und 0 gelöschten Zeilen
2
.cargo/config.toml
Normale Datei
2
.cargo/config.toml
Normale Datei
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
rustflags = ["--cfg", "tokio_unstable"]
|
2
.gitignore
gevendort
Normale Datei
2
.gitignore
gevendort
Normale Datei
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/tmp
|
17
.rustfmt.toml
Normale Datei
17
.rustfmt.toml
Normale Datei
|
@ -0,0 +1,17 @@
|
||||||
|
array_width = 0
|
||||||
|
binop_separator= "Back"
|
||||||
|
brace_style="SameLineWhere"
|
||||||
|
chain_width = 0
|
||||||
|
fn_params_layout = "Tall"
|
||||||
|
force_explicit_abi = true
|
||||||
|
hard_tabs = false
|
||||||
|
imports_layout = "Vertical"
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
max_width = 240
|
||||||
|
merge_derives = true
|
||||||
|
newline_style = "Unix"
|
||||||
|
remove_nested_parens = true
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
single_line_if_else_max_width = 0
|
||||||
|
tab_spaces = 4
|
2751
Cargo.lock
generiert
Normale Datei
2751
Cargo.lock
generiert
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
122
Cargo.toml
Normale Datei
122
Cargo.toml
Normale Datei
|
@ -0,0 +1,122 @@
|
||||||
|
[package]
|
||||||
|
name = "anime_calendar"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[dependencies.icalendar]
|
||||||
|
version = "~0.16"
|
||||||
|
|
||||||
|
[dependencies.systemd-journal-logger]
|
||||||
|
version = "~2.2"
|
||||||
|
|
||||||
|
[dependencies.toml]
|
||||||
|
version = "~0.8"
|
||||||
|
|
||||||
|
[dependencies.lazy_static]
|
||||||
|
version = "~1.5"
|
||||||
|
|
||||||
|
[dependencies.stderrlog]
|
||||||
|
version = "0.*"
|
||||||
|
|
||||||
|
[dependencies.url]
|
||||||
|
version = "2.*"
|
||||||
|
|
||||||
|
[dependencies.strfmt]
|
||||||
|
version = "0.2.4"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4.*"
|
||||||
|
default-features = false
|
||||||
|
features = ["std", "max_level_trace", "release_max_level_trace"]
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
version = "4.5.*"
|
||||||
|
default-features = false
|
||||||
|
features = ["derive", "cargo", "std"]
|
||||||
|
|
||||||
|
[dependencies.clap_builder]
|
||||||
|
version = "4.5.*"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4.*"
|
||||||
|
default-features = false
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
|
[dependencies.myanimelist]
|
||||||
|
version = "0.1.*"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.12.*"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.muddy]
|
||||||
|
version = "*"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
|
||||||
|
# Webservers
|
||||||
|
|
||||||
|
[dependencies.hyper]
|
||||||
|
version = "1.5.1"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.http-body-util]
|
||||||
|
optional = true
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[dependencies.hyper-util]
|
||||||
|
optional = true
|
||||||
|
version = "0.1"
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
|
[dependencies.queryst]
|
||||||
|
optional = true
|
||||||
|
version = "3.0.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
|
||||||
|
[build-dependencies.tokio]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[build-dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[build-dependencies.vaultrs]
|
||||||
|
version = "0.7.*"
|
||||||
|
optional = true
|
||||||
|
default-features = false
|
||||||
|
features = ["native-tls"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
mal = ["dep:myanimelist", "hyper", "dep:reqwest", "secrets"]
|
||||||
|
hyper = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:queryst"]
|
||||||
|
secrets = ["dep:vaultrs"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[workspace.dependencies.tokio]
|
||||||
|
version = "1.41"
|
||||||
|
default-features = false
|
||||||
|
features = ["rt"]
|
||||||
|
|
||||||
|
[workspace.dependencies.serde]
|
||||||
|
version = "1.0.*"
|
||||||
|
default-features = false
|
||||||
|
features = ["derive", "std"]
|
47
build.rs
Normale Datei
47
build.rs
Normale Datei
|
@ -0,0 +1,47 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::runtime::LocalRuntime;
|
||||||
|
#[cfg(feature = "secrets")]
|
||||||
|
use vaultrs::{
|
||||||
|
client::{VaultClient, VaultClientSettingsBuilder},
|
||||||
|
error::ClientError,
|
||||||
|
kv2,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct MALKey {
|
||||||
|
clientid: String,
|
||||||
|
clientsecret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MALKey {
|
||||||
|
fn default() -> Self {
|
||||||
|
MALKey {
|
||||||
|
clientid: "UNSET".into(),
|
||||||
|
clientsecret: "UNSET".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let runtime = LocalRuntime::new().expect("Local runtime failed to setup");
|
||||||
|
let mut data = MALKey::default();
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
{
|
||||||
|
let client = VaultClient::new(
|
||||||
|
VaultClientSettingsBuilder::default()
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let secret: Result<MALKey, ClientError> = runtime.block_on(async { kv2::read(&client, "builds", "myanimelist").await });
|
||||||
|
match secret {
|
||||||
|
Ok(vaultdata) => data = vaultdata,
|
||||||
|
Err(error) => {
|
||||||
|
println!("cargo::warning=Failed to get secret from the vault: {}", error.to_string());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
println!("cargo::rustc-env=MAL_CLIENTID={}\ncargo::rustc-env=MAL_CLIENTSECRET={}", data.clientid, data.clientsecret);
|
||||||
|
}
|
160
src/config.rs
Normale Datei
160
src/config.rs
Normale Datei
|
@ -0,0 +1,160 @@
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use chrono::{FixedOffset, NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
use myanimelist::{auth::Auth, objects::AnimeNode, AccessToken, RefreshToken};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{fmt::Debug, str::FromStr};
|
||||||
|
|
||||||
|
fn default_filename() -> String {
|
||||||
|
return "all.ics".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_length() -> i64 {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_shift() -> String {
|
||||||
|
return "+0100".into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialOrd, Ord)]
|
||||||
|
pub(crate) struct Anime {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) episodes: u64,
|
||||||
|
pub(crate) start: chrono::NaiveDateTime,
|
||||||
|
#[serde(default = "default_length")]
|
||||||
|
pub(crate) length: i64,
|
||||||
|
#[serde(default = "default_shift")]
|
||||||
|
shift: String,
|
||||||
|
pub(crate) url: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) pause: Vec<chrono::NaiveDate>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Anime {
|
||||||
|
pub(crate) fn shift(&self) -> FixedOffset {
|
||||||
|
return FixedOffset::from_str(
|
||||||
|
self.shift
|
||||||
|
.clone()
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
impl From<AnimeNode> for Anime {
|
||||||
|
fn from(value: AnimeNode) -> Self {
|
||||||
|
let offset: FixedOffset = FixedOffset::east_opt(9 * 60 * 60).unwrap();
|
||||||
|
let broadcast = value
|
||||||
|
.broadcast
|
||||||
|
.unwrap();
|
||||||
|
let start_date = value
|
||||||
|
.start_date
|
||||||
|
.unwrap();
|
||||||
|
let mut title = value.title;
|
||||||
|
if let Some(alt_titles) = value.alternative_titles {
|
||||||
|
if let Some(en_title) = alt_titles.en {
|
||||||
|
title = en_title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
name: title,
|
||||||
|
episodes: match value.num_episodes {
|
||||||
|
None => 13,
|
||||||
|
Some(0) => 13,
|
||||||
|
Some(i) => i.into(),
|
||||||
|
},
|
||||||
|
start: NaiveDateTime::new(
|
||||||
|
NaiveDate::from_ymd_opt(
|
||||||
|
start_date
|
||||||
|
.year
|
||||||
|
.into(),
|
||||||
|
start_date
|
||||||
|
.month
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
start_date
|
||||||
|
.day
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
broadcast.start_time,
|
||||||
|
)
|
||||||
|
.checked_sub_offset(offset)
|
||||||
|
.unwrap(),
|
||||||
|
length: match value.average_episode_duration {
|
||||||
|
Some(i) => (i as i64).div_euclid(60),
|
||||||
|
None => 25,
|
||||||
|
},
|
||||||
|
shift: "+0100".into(),
|
||||||
|
url: Some(format!("https://myanimelist.net/anime/{}", value.id)),
|
||||||
|
pause: Vec::new(),
|
||||||
|
description: value.synopsis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Anime {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name == other.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
pub(crate) struct MALAccesskey {
|
||||||
|
pub(crate) access_token: AccessToken,
|
||||||
|
pub(crate) refresh_token: RefreshToken,
|
||||||
|
pub(crate) access_expiry_time: u64,
|
||||||
|
pub(crate) refresh_expiry_time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
impl MALAccesskey {
|
||||||
|
pub(crate) fn load(&self, auth: &Auth) {
|
||||||
|
auth.set_access_token_unchecked(
|
||||||
|
self.access_token
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
auth.set_expires_at_unchecked(self.access_expiry_time);
|
||||||
|
auth.set_refresh_token_unchecked(
|
||||||
|
self.refresh_token
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
auth.set_refresh_expires_at_unchecked(self.refresh_expiry_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub(crate) struct Config {
|
||||||
|
pub(crate) outputdir: String,
|
||||||
|
#[serde(default = "default_filename")]
|
||||||
|
pub(crate) complete_ics: String,
|
||||||
|
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
pub(crate) mal_accesskey: Option<MALAccesskey>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) anime: Vec<Anime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Parser, Debug)]
|
||||||
|
#[command(author, version, about, disable_help_subcommand = true)]
|
||||||
|
pub(crate) struct Commandline {
|
||||||
|
#[arg(short, long, default_value = "/etc/animecalendar.toml")]
|
||||||
|
pub(crate) config: String,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub(crate) subcommand: Option<Subcommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Debug, Clone, Copy)]
|
||||||
|
pub(crate) enum Subcommand {
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
#[command(name="sync", visible_aliases=["sync-mal","mal"] ,about="Adds the missing shows from MyAnimelist")]
|
||||||
|
SyncMAL,
|
||||||
|
}
|
249
src/main.rs
Normale Datei
249
src/main.rs
Normale Datei
|
@ -0,0 +1,249 @@
|
||||||
|
use chrono::Duration;
|
||||||
|
use clap::{crate_name, Parser};
|
||||||
|
use icalendar::{Calendar, Component, Event, EventLike, Property};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{self};
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
use malclient::update_config;
|
||||||
|
use muddy::muddy_init;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs,
|
||||||
|
io::{self, ErrorKind, Read, Seek, Write},
|
||||||
|
os::unix::fs::OpenOptionsExt,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
use stderrlog::{new as errlog, ColorChoice, Timestamp};
|
||||||
|
use systemd_journal_logger::JournalLog;
|
||||||
|
use tokio::runtime::LocalRuntime;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
mod malclient;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CONFIG_READOPTIONS: fs::OpenOptions = fs::OpenOptions::new()
|
||||||
|
.write(false)
|
||||||
|
.read(true)
|
||||||
|
.create(false)
|
||||||
|
.to_owned();
|
||||||
|
static ref CALENDAR_WRITEOPTIONS: fs::OpenOptions = fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.read(false)
|
||||||
|
.mode(0o644)
|
||||||
|
.to_owned();
|
||||||
|
static ref CONFIG_WRITEOPTIONS: fs::OpenOptions = fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.read(true)
|
||||||
|
.mode(0o644)
|
||||||
|
.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
muddy_init!("embed");
|
||||||
|
|
||||||
|
fn slugify(input: String) -> String {
|
||||||
|
input
|
||||||
|
.to_lowercase()
|
||||||
|
.replace(" ", "-")
|
||||||
|
.replace(",", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_cal(name: &str, description: &str) -> Calendar {
|
||||||
|
Calendar::new()
|
||||||
|
.name(name)
|
||||||
|
.description(description)
|
||||||
|
.ttl(&Duration::days(1))
|
||||||
|
.append_property(Property::new("CUTYPE", "OTHER"))
|
||||||
|
.append_property(Property::new("FBTYPE", "BUSY-UNAVAILABLE"))
|
||||||
|
.done()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_calendars(config: config::Config) {
|
||||||
|
match fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.create(
|
||||||
|
config
|
||||||
|
.outputdir
|
||||||
|
.clone(),
|
||||||
|
) {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(err) => match err.kind() {
|
||||||
|
ErrorKind::AlreadyExists => {},
|
||||||
|
error => {
|
||||||
|
log::error!("Failed to create directory: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let binding = config
|
||||||
|
.outputdir
|
||||||
|
.clone();
|
||||||
|
let p = Path::new(&binding);
|
||||||
|
let mut allcal = gen_cal("Anime Calendar", "Calendar for all configured animes");
|
||||||
|
let mut itemcal: Calendar;
|
||||||
|
let mut formatargs = HashMap::<String, String>::with_capacity(2);
|
||||||
|
formatargs.insert("name".to_string(), "".to_string());
|
||||||
|
formatargs.insert("episode".to_string(), "".to_string());
|
||||||
|
for anime in config
|
||||||
|
.anime
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
itemcal = gen_cal(format!("Calendar for {}", anime.name).as_str(), format!("Calendar for {}", anime.name).as_str());
|
||||||
|
formatargs.insert(
|
||||||
|
"name".to_string(),
|
||||||
|
anime
|
||||||
|
.name
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
let mut lastdate = anime
|
||||||
|
.start
|
||||||
|
.checked_add_offset(anime.shift())
|
||||||
|
.unwrap();
|
||||||
|
let mut pause = anime
|
||||||
|
.pause
|
||||||
|
.clone();
|
||||||
|
pause.sort();
|
||||||
|
for episode in 1..(anime.episodes + 1) {
|
||||||
|
formatargs.insert("episode".to_string(), episode.to_string());
|
||||||
|
let mut start = if episode == 1 {
|
||||||
|
lastdate
|
||||||
|
} else {
|
||||||
|
lastdate
|
||||||
|
.checked_add_days(chrono::Days::new(7))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
for breakdate in pause.iter() {
|
||||||
|
if start.date() == *breakdate {
|
||||||
|
start = start
|
||||||
|
.checked_add_days(chrono::Days::new(7))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastdate = start;
|
||||||
|
let mut item = Event::new()
|
||||||
|
.starts(start)
|
||||||
|
.ends(
|
||||||
|
start
|
||||||
|
.checked_add_signed(Duration::minutes(anime.length))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.summary(
|
||||||
|
anime
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.alarm(icalendar::Alarm::display(&anime.name, Duration::seconds(0)))
|
||||||
|
.uid(slugify(format!("{}-{}", anime.name, episode)).as_str())
|
||||||
|
.done();
|
||||||
|
match anime
|
||||||
|
.description
|
||||||
|
.clone()
|
||||||
|
{
|
||||||
|
None => {},
|
||||||
|
Some(desc) => {
|
||||||
|
item.description(desc.as_str());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
match anime
|
||||||
|
.url
|
||||||
|
.clone()
|
||||||
|
{
|
||||||
|
None => {},
|
||||||
|
Some(url) => {
|
||||||
|
let url = strfmt::strfmt(url.as_str(), &formatargs.clone()).unwrap();
|
||||||
|
item.url(url.as_str())
|
||||||
|
.location(url.as_str());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
allcal.push(item.clone());
|
||||||
|
itemcal.push(item.done());
|
||||||
|
}
|
||||||
|
let mut itemics = CALENDAR_WRITEOPTIONS
|
||||||
|
.open(p.join(format!("{}.ics", anime.name.clone())))
|
||||||
|
.unwrap();
|
||||||
|
write!(itemics, "{}", itemcal).unwrap();
|
||||||
|
}
|
||||||
|
let mut allics = CALENDAR_WRITEOPTIONS
|
||||||
|
.open(p.join(format!("{}", config.complete_ics)))
|
||||||
|
.unwrap();
|
||||||
|
write!(allics, "{}", allcal).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
match JournalLog::new() {
|
||||||
|
Ok(log) => {
|
||||||
|
log.add_extra_field("RUST_PACKAGE", crate_name!())
|
||||||
|
.install()
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
io::stderr()
|
||||||
|
.write(b"Failed to setup logging")
|
||||||
|
.unwrap();
|
||||||
|
errlog()
|
||||||
|
.color(ColorChoice::Auto)
|
||||||
|
.quiet(false)
|
||||||
|
.show_level(true)
|
||||||
|
.show_module_names(true)
|
||||||
|
.timestamp(Timestamp::Second)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
log::set_max_level(log::LevelFilter::Trace);
|
||||||
|
let args = config::Commandline::parse();
|
||||||
|
let configfileresult = match args.subcommand {
|
||||||
|
None => CONFIG_READOPTIONS.open(args.config),
|
||||||
|
Some(_) => CONFIG_WRITEOPTIONS.open(args.config),
|
||||||
|
};
|
||||||
|
let mut configfile = match configfileresult {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error opening the configuration: {}", e.to_string());
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut cfgstring = String::new();
|
||||||
|
match configfile.read_to_string(&mut cfgstring) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Failed to read config file: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut config: config::Config = toml::from_str(&cfgstring).expect("Expected valid toml");
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let runtime = LocalRuntime::new().expect("Failed to get LocalRuntime");
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
match args.subcommand {
|
||||||
|
None => {
|
||||||
|
log::info!("ICal creator started up");
|
||||||
|
log::debug!("{:?}", config);
|
||||||
|
write_calendars(config);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
#[cfg(feature = "mal")]
|
||||||
|
Some(config::Subcommand::SyncMAL) => {
|
||||||
|
log::info!("Updating the list");
|
||||||
|
let _guard = runtime.enter();
|
||||||
|
runtime.block_on(update_config(&mut config));
|
||||||
|
},
|
||||||
|
Some(_) => {},
|
||||||
|
}
|
||||||
|
match toml::to_string_pretty(&config) {
|
||||||
|
Ok(configstring) => {
|
||||||
|
let _ = configfile.seek(io::SeekFrom::Start(0));
|
||||||
|
let _ = configfile.set_len(0);
|
||||||
|
let _ = configfile.write(configstring.as_bytes());
|
||||||
|
let _ = configfile.sync_all();
|
||||||
|
},
|
||||||
|
Err(_) => todo!(),
|
||||||
|
};
|
||||||
|
}
|
116
src/malclient/hyperserver.rs
Normale Datei
116
src/malclient/hyperserver.rs
Normale Datei
|
@ -0,0 +1,116 @@
|
||||||
|
use crate::malclient::LOCALHOSTV4;
|
||||||
|
use http_body_util::Full;
|
||||||
|
use hyper::{body::Bytes, http::Error as HttpError, server::conn::http1, service::service_fn, Request, Response};
|
||||||
|
use hyper_util::rt::TokioIo;
|
||||||
|
use log::{debug, error};
|
||||||
|
use myanimelist::AuthorizationCode;
|
||||||
|
use queryst::parse;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
use super::{CALLBACKRESULT, CSRFTOKEN};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn response(text: &'static str, status: u16) -> Result<Response<Full<Bytes>>, HttpError> {
|
||||||
|
Response::builder()
|
||||||
|
.status(status)
|
||||||
|
.header("Content-Type", "text/plain")
|
||||||
|
.body(Full::new(Bytes::from(text)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn service(request: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, HttpError> {
|
||||||
|
if request
|
||||||
|
.method()
|
||||||
|
.as_str()
|
||||||
|
!= "GET"
|
||||||
|
|| request
|
||||||
|
.uri()
|
||||||
|
.query()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return response("Sorry this must be redirected from MyAnimelist", 400);
|
||||||
|
}
|
||||||
|
let token = match CSRFTOKEN.lock() {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to get token: {}", error);
|
||||||
|
return response("Internal Server Error", 500);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let query = match parse(
|
||||||
|
request
|
||||||
|
.uri()
|
||||||
|
.query()
|
||||||
|
.unwrap(),
|
||||||
|
) {
|
||||||
|
Ok(query) => query,
|
||||||
|
Err(error) => {
|
||||||
|
error!("failed to parse Query: {}", error.message);
|
||||||
|
return response("Sorry this must be redirected from MyAnimelist", 400);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Some(querytoken) = query.get("state") {
|
||||||
|
if querytoken
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
!= token.secret()
|
||||||
|
{
|
||||||
|
debug!(
|
||||||
|
"Querytoken != token: {} != {}",
|
||||||
|
querytoken
|
||||||
|
.as_str()
|
||||||
|
.unwrap(),
|
||||||
|
token.secret()
|
||||||
|
);
|
||||||
|
return response("Invalid Token, please retry with the latest displayed link", 400);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return response("Sorry this must be redirected from MyAnimelist", 400);
|
||||||
|
}
|
||||||
|
let mut callback = CALLBACKRESULT
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
*callback = AuthorizationCode::new(
|
||||||
|
query
|
||||||
|
.get("code")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
response("Successful. You can now close this Tab", 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn hyperserver() -> Result<(), ()> {
|
||||||
|
let sock = match TcpListener::bind(LOCALHOSTV4).await {
|
||||||
|
Ok(sock) => sock,
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to listen ot socket: {}", error);
|
||||||
|
return Err(());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut connparser = http1::Builder::new();
|
||||||
|
connparser.auto_date_header(true);
|
||||||
|
connparser.keep_alive(false);
|
||||||
|
loop {
|
||||||
|
let stream = match sock
|
||||||
|
.accept()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((stream, _)) => TokioIo::new(stream),
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to accept connection: {}", error);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
match connparser
|
||||||
|
.clone()
|
||||||
|
.serve_connection(stream, service_fn(service))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to process request: {}", error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
253
src/malclient/mod.rs
Normale Datei
253
src/malclient/mod.rs
Normale Datei
|
@ -0,0 +1,253 @@
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
net::{Ipv4Addr, SocketAddr},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::config::{self, Anime, MALAccesskey};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use muddy::{muddy_all, muddy_init};
|
||||||
|
use myanimelist::{
|
||||||
|
self,
|
||||||
|
auth::Auth,
|
||||||
|
objects::{Username, WatchStatus},
|
||||||
|
AuthorizationCode, ClientId, ClientSecret, CsrfToken, MalClient, MalClientError, RedirectUrl, Scope,
|
||||||
|
};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tokio::{task, time::sleep};
|
||||||
|
|
||||||
|
#[cfg(feature = "hyper")]
|
||||||
|
mod hyperserver;
|
||||||
|
#[cfg(feature = "hyper")]
|
||||||
|
use hyperserver::hyperserver as serverfuture;
|
||||||
|
|
||||||
|
const SCHEME: &str = "http";
|
||||||
|
const HOST: &str = "localhost";
|
||||||
|
const PORT: u16 = 8080;
|
||||||
|
const LOCALHOSTV4: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), PORT);
|
||||||
|
const LIMIT: u16 = 100;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CALLBACKRESULT: Mutex<AuthorizationCode> = Mutex::new(AuthorizationCode::new(String::new()));
|
||||||
|
static ref CSRFTOKEN: Mutex<CsrfToken> = Mutex::new(CsrfToken::new(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
muddy_init!();
|
||||||
|
|
||||||
|
muddy_all! {
|
||||||
|
static CLIENTID: &str = env!("MAL_CLIENTID");
|
||||||
|
static CLIENTSECRET: &str = env!("MAL_CLIENTSECRET");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn callback(_url: reqwest::Url, token: CsrfToken) -> Result<(AuthorizationCode, CsrfToken), Box<dyn Error>> {
|
||||||
|
{
|
||||||
|
let mut csrftoken = CSRFTOKEN
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
*csrftoken = CsrfToken::new(
|
||||||
|
token
|
||||||
|
.secret()
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!("Please visit {} to authenticate", _url.as_str());
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_nanos(100)).await;
|
||||||
|
let lock = match CALLBACKRESULT.lock() {
|
||||||
|
Ok(cg) => cg,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Failed to get call guard: {error}");
|
||||||
|
return Err(Box::new(error));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if lock
|
||||||
|
.secret()
|
||||||
|
.len()
|
||||||
|
!= 0
|
||||||
|
{
|
||||||
|
return Ok((
|
||||||
|
AuthorizationCode::new(
|
||||||
|
lock.secret()
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
token,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_animes(client: &MalClient, animelist: &mut Vec<Anime>, status: WatchStatus) {
|
||||||
|
let mut offset: u64 = 0;
|
||||||
|
loop {
|
||||||
|
debug!("Getting animes, round {}", offset / LIMIT as u64);
|
||||||
|
match client
|
||||||
|
.user_animelist()
|
||||||
|
.get()
|
||||||
|
.user_name(Username::Me)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(LIMIT)
|
||||||
|
.nsfw(true)
|
||||||
|
.status(status)
|
||||||
|
.fields([
|
||||||
|
"title",
|
||||||
|
"num_episodes",
|
||||||
|
"average_episode_duration",
|
||||||
|
"id",
|
||||||
|
"broadcast",
|
||||||
|
"start_date",
|
||||||
|
"synopsis",
|
||||||
|
"alternative_titles",
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(list) => {
|
||||||
|
for aitem in list.data {
|
||||||
|
let anime = aitem.node;
|
||||||
|
if anime.start_date == None {
|
||||||
|
info!("{} has no start date", anime.title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start_date = anime
|
||||||
|
.start_date
|
||||||
|
.clone()
|
||||||
|
.unwrap();
|
||||||
|
if start_date.day == None || start_date.month == None {
|
||||||
|
info!("{} has no start date", anime.title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if anime.broadcast == None {
|
||||||
|
info!("{} has no broadcast time", anime.title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
debug!(
|
||||||
|
"Adding {}",
|
||||||
|
anime
|
||||||
|
.title
|
||||||
|
.clone()
|
||||||
|
);
|
||||||
|
animelist.push(anime.into());
|
||||||
|
}
|
||||||
|
match list.paging {
|
||||||
|
Some(paging) => {
|
||||||
|
if paging
|
||||||
|
.next
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += LIMIT as u64;
|
||||||
|
},
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to get {} items: {}", status, error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_config(config: &mut config::Config) {
|
||||||
|
let server = task::spawn_local(serverfuture());
|
||||||
|
|
||||||
|
let auth: Auth = Auth::new(
|
||||||
|
ClientId::new(CLIENTID.to_string()),
|
||||||
|
ClientSecret::new(CLIENTSECRET.to_string()),
|
||||||
|
RedirectUrl::new(format!("{SCHEME}://{HOST}:{PORT}/")).unwrap(),
|
||||||
|
);
|
||||||
|
auth.add_scope(Scope::new("write:users".to_string()));
|
||||||
|
auth.set_callback(callback)
|
||||||
|
.await;
|
||||||
|
if config
|
||||||
|
.mal_accesskey
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
config
|
||||||
|
.mal_accesskey
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.load(&auth);
|
||||||
|
}
|
||||||
|
if !auth.is_access_valid() {
|
||||||
|
if auth.is_refresh_valid() {
|
||||||
|
match auth
|
||||||
|
.refresh()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Failed to refresh token: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.mal_accesskey = Some(MALAccesskey {
|
||||||
|
access_token: auth.access_token(),
|
||||||
|
refresh_token: auth.refresh_token(),
|
||||||
|
access_expiry_time: auth.expires_at(),
|
||||||
|
refresh_expiry_time: auth.refresh_expires_at(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
match auth
|
||||||
|
.regenerate()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
config.mal_accesskey = Some(MALAccesskey {
|
||||||
|
access_token: auth.access_token(),
|
||||||
|
refresh_token: auth.refresh_token(),
|
||||||
|
access_expiry_time: auth.expires_at(),
|
||||||
|
refresh_expiry_time: auth.refresh_expires_at(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Err(error) => {
|
||||||
|
error!("Failed to update the token: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = server.abort();
|
||||||
|
let _client = match myanimelist::MalClientBuilder::new()
|
||||||
|
.auth(auth)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(MalClientError::Reqwest(error)) => {
|
||||||
|
log::error!("Failed to connect to the API: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
Err(MalClientError::Builder(error)) => {
|
||||||
|
log::error!("Failed to build object: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
Err(MalClientError::Token(error)) => {
|
||||||
|
log::error!("Broken token: {}", error);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut animelist = Vec::<Anime>::with_capacity(20);
|
||||||
|
debug!("getting all planned animes");
|
||||||
|
get_animes(&_client, &mut animelist, WatchStatus::PlanToWatch).await;
|
||||||
|
debug!("Getting currently watched animes");
|
||||||
|
get_animes(&_client, &mut animelist, WatchStatus::Watching).await;
|
||||||
|
debug!("Anime List: {:?}", animelist);
|
||||||
|
for anime in animelist {
|
||||||
|
if !config
|
||||||
|
.anime
|
||||||
|
.contains(&anime)
|
||||||
|
{
|
||||||
|
config
|
||||||
|
.anime
|
||||||
|
.insert(0, anime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config
|
||||||
|
.anime
|
||||||
|
.sort();
|
||||||
|
config
|
||||||
|
.anime
|
||||||
|
.dedup();
|
||||||
|
}
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren