use super::Series;
use crate::{
support::{empty_to_none, Cursors},
Annict, Chapter,
Error::*,
Media, Season, Status, Work,
};
use graphql_client::{GraphQLQuery, Response};
use reqwest::Client;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/get_works_from_series.graphql",
variables_derives = "Clone"
)]
struct GetWorksFromSeries;
pub struct Paginator<'ep> {
client: Client,
endpoint: &'ep str,
variables: get_works_from_series::Variables,
cursors: Option<Cursors>,
}
impl Series {
pub fn works(&self) -> get_works_from_series::Variables {
get_works_from_series::Variables {
id: self.internal_id.clone(),
items: 0,
prev: None,
next: None,
}
}
}
impl get_works_from_series::Variables {
pub async fn search(mut self, annict: &Annict<'_>, items: u8) -> crate::Result<Vec<Work>> {
self.items = items as i64;
get_works(annict.client.clone(), annict.endpoint, self)
.await
.map(|(w, _)| w)
}
pub fn paginate<'ep>(mut self, annict: &Annict<'ep>, items_per_page: u8) -> Paginator<'ep> {
self.items = items_per_page as i64;
Paginator {
client: annict.client.clone(),
endpoint: annict.endpoint,
variables: self,
cursors: None,
}
}
}
impl Paginator<'_> {
pub async fn prev(&mut self) -> crate::Result<Vec<Work>> {
self.variables.prev = match self.cursors.as_ref() {
Some(Cursors { prev: None, .. }) | None => return Ok(Vec::with_capacity(0)),
Some(Cursors { prev, .. }) => prev.clone(),
};
self.variables.next = None;
let (works, page_info) =
get_works(self.client.clone(), self.endpoint, self.variables.clone()).await?;
self.cursors = Some(page_info);
Ok(works)
}
pub async fn next(&mut self) -> crate::Result<Vec<Work>> {
self.variables.next = match self.cursors.as_ref() {
Some(Cursors { next: None, .. }) => return Ok(Vec::with_capacity(0)),
Some(Cursors { next, .. }) => next.clone(),
None => None,
};
self.variables.prev = None;
let (works, page_info) =
get_works(self.client.clone(), self.endpoint, self.variables.clone()).await?;
self.cursors = Some(page_info);
Ok(works)
}
}
async fn get_works(
client: Client,
endpoint: &str,
variables: get_works_from_series::Variables,
) -> crate::Result<(Vec<Work>, Cursors)> {
let resp = client
.post(endpoint)
.json(&GetWorksFromSeries::build_query(variables))
.send()
.await?;
let body: Response<get_works_from_series::ResponseData> = resp.json().await?;
if let Some(err) = body.errors {
return Err(ErrorResponse(err));
}
let Some(data) = body.data else {
return Err(UnexpectedResponse("no data".to_owned()));
};
let Some(node) = data.node else {
return Err(UnexpectedResponse("data.node is None".to_owned()));
};
let works = match node {
get_works_from_series::GetWorksFromSeriesNode::Series(s) => s.works,
_ => {
return Err(UnexpectedResponse(
"query result should be Series".to_owned(),
))
}
};
let Some(works) = works else {
return Err(UnexpectedResponse("no works".to_owned()));
};
let page_info = Cursors {
prev: works.page_info.start_cursor,
next: works.page_info.end_cursor,
};
let Some(nodes) = works.nodes else {
return Err(UnexpectedResponse("no nodes".to_owned()));
};
let works = nodes
.into_iter()
.flatten()
.map(|work| {
let season = match work.season_name {
Some(get_works_from_series::SeasonName::SPRING) => Season::Spring,
Some(get_works_from_series::SeasonName::SUMMER) => Season::Summer,
Some(get_works_from_series::SeasonName::AUTUMN) => Season::Autumn,
Some(get_works_from_series::SeasonName::WINTER) => Season::Winter,
Some(get_works_from_series::SeasonName::Other(s)) => return Err(UnknownSeason(s)),
None => Season::Unspecified,
};
let media = match work.media {
get_works_from_series::Media::TV => Media::Tv,
get_works_from_series::Media::OVA => Media::Ova,
get_works_from_series::Media::MOVIE => Media::Movie,
get_works_from_series::Media::WEB => Media::Web,
get_works_from_series::Media::OTHER => Media::Other,
get_works_from_series::Media::Other(s) => return Err(UnknownMedia(s)),
};
let chapter = if work.no_episodes {
Chapter::Single
} else {
Chapter::Episodes(work.episodes_count)
};
let Some(my_status) = work
.viewer_status_state
.map(|status| match status {
get_works_from_series::StatusState::NO_STATE => Ok(Status::NotSet),
get_works_from_series::StatusState::WANNA_WATCH => Ok(Status::WannaWatch),
get_works_from_series::StatusState::WATCHING => Ok(Status::Watching),
get_works_from_series::StatusState::ON_HOLD => Ok(Status::OnHold),
get_works_from_series::StatusState::WATCHED => Ok(Status::Watched),
get_works_from_series::StatusState::STOP_WATCHING => Ok(Status::Stopped),
get_works_from_series::StatusState::Other(s) => Err(UnknownStatus(s)),
})
.transpose()?
else {
return Err(Unexpected(
"it seems Annict always returns a non-null status".to_owned(),
));
};
let (image, x_avatar) = match work.image.map(|s| {
(
s.recommended_image_url.and_then(empty_to_none),
s.twitter_avatar_url.and_then(empty_to_none),
)
}) {
Some((i, x)) => (i, x),
None => (None, None),
};
Ok(Work {
id: work.annict_id,
title: work.title,
title_kana: work.title_kana.and_then(empty_to_none),
title_romaji: work.title_ro.and_then(empty_to_none),
year: work.season_year,
season,
media,
chapter,
my_status,
image,
x_id: work.twitter_username.and_then(empty_to_none),
x_avatar,
hashtag: work.twitter_hashtag.and_then(empty_to_none),
official_site: work.official_site_url.and_then(empty_to_none),
wikipedia: work.wikipedia_url.and_then(empty_to_none),
my_anime_list_id: work.mal_anime_id,
syoboi_calendar_id: work.syobocal_tid,
watchers: work.watchers_count,
reviews: work.reviews_count,
internal_id: work.id,
})
})
.collect::<Result<_, _>>()?;
Ok((works, page_info))
}