use std::ops::Deref;
use chrono::prelude::*;
use const_format::formatcp;
use reqwest::multipart;
use crate::{
errors::ApiError,
objects::{Action, AutoScores, ListAnime, ListAnimePut, ListStatus, UserListAnime},
rate_limit::RateLimit,
utils::IsJson as _,
AnimeScheduleClient, API_URL, RUNTIME,
};
const API_ANIMELISTS_USERID_ROUTE: &str = formatcp!("{API_URL}/animelists/{{userId}}/{{route}}");
const API_ANIMELISTS_ROUTE: &str = formatcp!("{API_URL}/animelists/oauth/{{route}}");
const API_ANIMELISTS_USERID: &str = formatcp!("{API_URL}/animelists/{{userId}}");
const API_ANIMELISTS: &str = formatcp!("{API_URL}/animelists/oauth");
pub struct AnimeListsApi {
client: AnimeScheduleClient,
}
impl AnimeListsApi {
pub(crate) fn new(client: AnimeScheduleClient) -> Self {
Self { client }
}
pub fn get(&self) -> AnimeListsGet {
AnimeListsGet {
client: self.client.clone(),
user_id: None,
}
}
pub fn put(&self) -> AnimeListsPut {
AnimeListsPut {
client: self.client.clone(),
user_id: None,
overwrite_mal_list: false,
xml: None,
}
}
pub fn delete(&self) -> AnimeListsDelete {
AnimeListsDelete {
client: self.client.clone(),
route: None,
user_id: None,
}
}
}
pub struct AnimeListsGet {
client: AnimeScheduleClient,
user_id: Option<String>,
}
impl AnimeListsGet {
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_owned());
self
}
pub fn route(self, route: &str) -> AnimeListsGetRoute {
AnimeListsGetRoute {
client: self.client.clone(),
user_id: self.user_id,
route: route.to_owned(),
}
}
pub async fn send(self) -> Result<(RateLimit, UserListAnime), ApiError> {
let is_user_id = self.user_id.is_some();
let url = if let Some(user_id) = self.user_id {
API_ANIMELISTS_USERID.replace("{userId}", &user_id)
} else {
API_ANIMELISTS.to_owned()
};
let response = self
.client
.http
.get(url)
.bearer_auth(if is_user_id {
self.client.auth.app_token().to_owned()
} else {
self.client
.auth
.access_token()
.ok_or(ApiError::AccessToken)?
.secret()
.clone()
})
.send()
.await?;
let headers = response.headers();
let limit = RateLimit::new(headers);
let text = response.text().await?;
if !text.is_json() {
return Err(ApiError::Api(text));
}
let user_list: UserListAnime = serde_json::from_str(&text)?;
Ok((limit.unwrap(), user_list))
}
pub fn send_blocking(self) -> Result<(RateLimit, UserListAnime), ApiError> {
RUNTIME.block_on(self.send())
}
}
#[derive(Debug)]
pub struct ETag(pub String);
impl Deref for ETag {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct AnimeListsGetRoute {
client: AnimeScheduleClient,
user_id: Option<String>,
route: String,
}
impl AnimeListsGetRoute {
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_owned());
self
}
pub async fn send(self) -> Result<(RateLimit, ETag, ListAnime), ApiError> {
let is_user_id = self.user_id.is_some();
let url = if let Some(user_id) = self.user_id {
API_ANIMELISTS_USERID_ROUTE
.replace("{userId}", &user_id)
.replace("{route}", &self.route)
} else {
API_ANIMELISTS_ROUTE.replace("{route}", &self.route)
};
let response = self
.client
.http
.get(url)
.bearer_auth(if is_user_id {
self.client.auth.app_token().to_owned()
} else {
self.client
.auth
.access_token()
.ok_or(ApiError::AccessToken)?
.secret()
.clone()
})
.send()
.await?;
let headers = response.headers();
let limit = RateLimit::new(headers);
let etag = ETag(
headers
.get("etag")
.and_then(|h| h.to_str().ok())
.unwrap_or_default()
.to_owned(),
);
let text = response.text().await?;
if !text.is_json() {
return Err(ApiError::Api(text));
}
let listanime: ListAnime = serde_json::from_str(&text)?;
Ok((limit.unwrap(), etag, listanime))
}
pub fn send_blocking(self) -> Result<(RateLimit, ETag, ListAnime), ApiError> {
RUNTIME.block_on(self.send())
}
}
pub struct AnimeListsPut {
client: AnimeScheduleClient,
user_id: Option<String>,
overwrite_mal_list: bool,
xml: Option<String>,
}
impl AnimeListsPut {
pub fn route(self, route: &str) -> AnimeListsPutRoute {
AnimeListsPutRoute {
client: self.client,
user_id: self.user_id,
route: route.to_owned(),
etag: None,
list: ListAnimePut::default(),
}
}
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_owned());
self
}
pub fn overwrite_mal_list(mut self, overwrite: bool) -> Self {
self.overwrite_mal_list = overwrite;
self
}
pub fn xml<S: Into<String>>(mut self, data: S) -> Self {
let data = data.into();
self.xml = Some(data);
self
}
pub async fn send(self) -> Result<RateLimit, ApiError> {
let url = if let Some(user_id) = self.user_id {
API_ANIMELISTS_USERID.replace("{userId}", &user_id)
} else {
API_ANIMELISTS.to_owned()
};
let Some(xml) = self.xml else {
return Err(ApiError::Xml);
};
let part = multipart::Part::bytes(xml.into_bytes())
.file_name("list.xml")
.mime_str("text/xml")
.unwrap();
let mut form = multipart::Form::new();
if self.overwrite_mal_list {
form = form.text("overwrite-mal-list", "on");
}
form = form.part("mal-list", part);
let response = self
.client
.http
.put(url)
.bearer_auth(
self.client
.auth
.access_token()
.ok_or(ApiError::AccessToken)?
.secret()
.clone(),
)
.multipart(form)
.send()
.await?;
let headers = response.headers();
let limit = RateLimit::new(headers);
let is_err = response.status().is_server_error() || response.status().is_client_error();
let text = response.text().await?;
if !text.is_empty() || is_err {
return Err(ApiError::Api(text));
}
Ok(limit.unwrap())
}
pub fn send_blocking(self) -> Result<RateLimit, ApiError> {
RUNTIME.block_on(self.send())
}
}
pub struct AnimeListsPutRoute {
client: AnimeScheduleClient,
user_id: Option<String>,
etag: Option<String>,
route: String,
list: ListAnimePut,
}
impl AnimeListsPutRoute {
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_owned());
self
}
pub fn etag(mut self, etag: &str) -> Self {
self.etag = Some(etag.to_owned());
self
}
pub fn list_status(mut self, status: ListStatus) -> Self {
self.list.list_status = Some(status);
self
}
pub fn episodes_seen(mut self, seen: u64) -> Self {
self.list.episodes_seen = Some(seen);
self
}
pub fn manual_score(mut self, score: u8) -> Self {
self.list.manual_score = Some(score.clamp(0, 100));
self
}
pub fn use_auto_scores(mut self, use_auto_scores: bool) -> Self {
self.list.use_auto_scores = Some(use_auto_scores);
self
}
pub fn auto_scores(mut self, scores: AutoScores) -> Self {
self.list.auto_scores = Some(scores);
self
}
pub fn start_date<Tz: TimeZone>(mut self, datetime: DateTime<Tz>) -> Self {
let datetime = datetime.with_timezone(&Utc);
self.list.start_date = Some(datetime);
self
}
pub fn end_date<Tz: TimeZone>(mut self, datetime: DateTime<Tz>) -> Self {
let datetime = datetime.with_timezone(&Utc);
self.list.end_date = Some(datetime);
self
}
pub fn note(mut self, note: &str) -> Self {
let mut note = note.to_owned();
note.truncate(1000);
self.list.note = Some(note);
self
}
pub fn action(mut self, action: Action) -> Self {
self.list.action = Some(action);
self
}
pub async fn send(self) -> Result<RateLimit, ApiError> {
if self.etag.is_none() {
return Err(ApiError::Etag);
}
let url = if let Some(user_id) = self.user_id {
API_ANIMELISTS_USERID_ROUTE
.replace("{userId}", &user_id)
.replace("{route}", &self.route)
} else {
API_ANIMELISTS_ROUTE.replace("{route}", &self.route)
};
let response = self
.client
.http
.put(url)
.header("ETag", self.etag.unwrap())
.bearer_auth(
self.client
.auth
.access_token()
.ok_or(ApiError::AccessToken)?
.secret()
.clone(),
)
.json(&self.list)
.send()
.await?;
let headers = response.headers();
let limit = RateLimit::new(headers);
let is_err = response.status().is_server_error() || response.status().is_client_error();
let text = response.text().await?;
if !text.is_empty() || is_err {
return Err(ApiError::Api(text));
}
Ok(limit.unwrap())
}
pub fn send_blocking(self) -> Result<RateLimit, ApiError> {
RUNTIME.block_on(self.send())
}
}
pub struct AnimeListsDelete {
client: AnimeScheduleClient,
route: Option<String>,
user_id: Option<String>,
}
impl AnimeListsDelete {
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_owned());
self
}
pub fn route(mut self, route: &str) -> Self {
self.route = Some(route.to_owned());
self
}
pub async fn send(self) -> Result<RateLimit, ApiError> {
let Some(route) = self.route else {
return Err(ApiError::Route);
};
let url = if let Some(user_id) = self.user_id {
API_ANIMELISTS_USERID_ROUTE
.replace("{userId}", &user_id)
.replace("{route}", &route)
} else {
API_ANIMELISTS_ROUTE.replace("{route}", &route)
};
let response = self
.client
.http
.delete(url)
.bearer_auth(
self.client
.auth
.access_token()
.ok_or(ApiError::AccessToken)?
.secret()
.clone(),
)
.send()
.await?;
let headers = response.headers();
let limit = RateLimit::new(headers);
let is_err = response.status().is_server_error() || response.status().is_client_error();
let text = response.text().await?;
if !text.is_empty() || is_err {
return Err(ApiError::Api(text));
}
Ok(limit.unwrap())
}
pub fn send_blocking(self) -> Result<RateLimit, ApiError> {
RUNTIME.block_on(self.send())
}
}