use serde::{Deserialize, Serialize};
use crate::{
directory::{DirectoryItem, DirectoryKind},
tag::TagItem,
utils::name_to_permalink,
DynastyReaderRoute, DYNASTY_READER_BASE,
};
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectoryListConfig {
pub name: String,
pub kind: DirectoryKind,
pub view_kind: Option<DirectoryListViewKind>,
pub page_number: u64,
}
impl DirectoryListConfig {
pub fn with_view_kind(self, view_kind: Option<DirectoryListViewKind>) -> DirectoryListConfig {
DirectoryListConfig {
name: self.name,
kind: self.kind,
view_kind,
page_number: self.page_number,
}
}
}
impl From<DirectoryItem> for DirectoryListConfig {
fn from(item: DirectoryItem) -> Self {
DirectoryListConfig {
name: item.name,
kind: item.kind,
view_kind: None,
page_number: 1,
}
}
}
impl From<TagItem> for DirectoryListConfig {
fn from(item: TagItem) -> Self {
DirectoryListConfig {
name: item.name,
kind: item.kind,
view_kind: None,
page_number: 1,
}
}
}
#[cfg(feature = "search")]
impl TryFrom<crate::search::SearchItem> for DirectoryListConfig {
type Error = anyhow::Error;
fn try_from(value: crate::search::SearchItem) -> Result<Self, Self::Error> {
if let crate::search::SearchCategory::Directory(kind) = value.kind {
Ok(DirectoryListConfig {
kind,
name: value.title,
page_number: 1,
view_kind: None,
})
} else {
Err(anyhow::anyhow!("this search item is a chapter"))
}
}
}
impl DynastyReaderRoute for DirectoryListConfig {
fn request_builder(
&self,
client: &reqwest::Client,
url: reqwest::Url,
) -> reqwest::RequestBuilder {
let builder = client.get(url).query(&[("page", self.page_number)]);
if let Some(view_kind) = self.view_kind {
builder.query(&[("view", view_kind.to_string())])
} else {
builder
}
}
fn request_url(&self) -> reqwest::Url {
let permalink = name_to_permalink(&self.name);
DYNASTY_READER_BASE
.join(&format!("{}/{}.json", self.kind, permalink))
.unwrap()
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectoryListViewKind {
Chapters,
Groupings,
OneShots,
Pairings,
}
impl std::fmt::Display for DirectoryListViewKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = {
use DirectoryListViewKind::*;
match self {
Chapters => "chapters",
Groupings => "groupings",
OneShots => "one_shots",
Pairings => "pairings",
}
};
write!(f, "{s}")
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DirectoryList {
pub name: String,
pub kind: DirectoryKind,
pub permalink: String,
pub tags: Vec<TagItem>,
pub status: Option<DirectoryListStatus>,
pub cover: Option<String>,
pub link: Option<String>,
pub description: Option<String>,
pub aliases: Vec<String>,
pub items: Vec<DirectoryListItem>,
pub chapter_items: Vec<DirectoryListChapterItem>,
pub page_number: u64,
pub max_page_number: u64,
}
impl<'de> Deserialize<'de> for DirectoryList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum DirectoryListChapterItemWrapper {
Chapter {
title: String,
permalink: String,
released_on: String,
tags: Vec<TagItem>,
},
Header {
header: Option<String>,
},
}
#[derive(Deserialize)]
#[serde(untagged)]
enum TagWrapper {
Default(TagItem),
Status(DirectoryListStatus),
}
#[derive(Deserialize)]
struct DirectoryListWrapper {
name: String,
#[serde(alias = "type")]
kind: DirectoryKind,
permalink: String,
tags: Vec<TagWrapper>,
#[serde(deserialize_with = "crate::utils::join_path_with_dynasty_reader_base")]
cover: String,
link: Option<String>,
description: Option<String>,
aliases: Vec<String>,
#[serde(default)]
taggables: Vec<DirectoryListItem>,
#[serde(default)]
taggings: Vec<DirectoryListChapterItemWrapper>,
current_page: Option<u64>,
total_pages: Option<u64>,
}
let wrapper: DirectoryListWrapper = Deserialize::deserialize(deserializer)?;
let DirectoryListWrapper {
name,
kind,
permalink,
tags,
cover,
link,
description,
aliases,
taggables: items,
taggings: chapter_items,
current_page: page_number,
total_pages: max_page_number,
} = wrapper;
let page_number = page_number.unwrap_or(1);
let max_page_number = max_page_number.unwrap_or(1);
let cover = if cover.is_empty() { None } else { Some(cover) };
let status = tags.iter().find_map(|tag| match tag {
TagWrapper::Status(s) => Some(s.clone()),
_ => None,
});
let tags = tags
.iter()
.filter_map(|tag| match tag {
TagWrapper::Default(s) => Some(s.clone()),
_ => None,
})
.collect();
let chapter_items = {
let mut current_header: Option<String> = None;
chapter_items.into_iter().fold(vec![], |mut current, item| {
use DirectoryListChapterItemWrapper::*;
match item {
Chapter {
title,
permalink,
released_on,
tags,
} => current.push(DirectoryListChapterItem {
header: current_header.clone(),
permalink,
released_on,
title,
tags,
}),
Header { header } => current_header = header,
}
current
})
};
Ok(DirectoryList {
name,
kind,
permalink,
tags,
status,
cover,
link,
description,
aliases,
items,
chapter_items,
page_number,
max_page_number,
})
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum DirectoryListStatus {
Abandoned,
Cancelled,
Completed,
Licensed,
Ongoing,
Unknown,
}
impl<'de> Deserialize<'de> for DirectoryListStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct DirectoryListStatusWrapper<'a> {
name: &'a str,
}
let wrapper: DirectoryListStatusWrapper = Deserialize::deserialize(deserializer)?;
Ok({
use DirectoryListStatus::*;
match wrapper.name {
"Abandoned" => Abandoned,
"Cancelled" => Cancelled,
"Completed" => Completed,
"Licensed" => Licensed,
"Ongoing" => Ongoing,
_ => Unknown,
}
})
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DirectoryListItem {
pub name: String,
pub kind: DirectoryKind,
pub permalink: String,
pub cover: Option<String>,
}
impl<'de> Deserialize<'de> for DirectoryListItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct DirectoryListItemWrapper {
name: String,
#[serde(alias = "type")]
kind: DirectoryKind,
permalink: String,
#[serde(deserialize_with = "crate::utils::join_path_with_dynasty_reader_base")]
cover: String,
}
let wrapper: DirectoryListItemWrapper = Deserialize::deserialize(deserializer)?;
let DirectoryListItemWrapper {
name,
kind,
permalink,
cover,
} = wrapper;
let cover = if cover.is_empty() { None } else { Some(cover) };
Ok(DirectoryListItem {
name,
kind,
permalink,
cover,
})
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct DirectoryListChapterItem {
pub title: String,
pub header: Option<String>,
pub permalink: String,
pub released_on: String,
pub tags: Vec<TagItem>,
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::test_utils::tryhard_configs;
use super::*;
fn create_config(
c: (
DirectoryKind,
&str,
impl Into<Option<DirectoryListViewKind>>,
),
) -> DirectoryListConfig {
DirectoryListConfig {
name: c.1.to_string(),
kind: c.0,
page_number: 1,
view_kind: c.2.into(),
}
}
fn create_config_with_superset_view_kind(c: (DirectoryKind, &str)) -> Vec<DirectoryListConfig> {
[
DirectoryListViewKind::Chapters,
DirectoryListViewKind::Groupings,
DirectoryListViewKind::OneShots,
DirectoryListViewKind::Pairings,
]
.map(|view_kind| create_config((c.0, c.1, view_kind)))
.to_vec()
}
#[tokio::test]
#[ignore = "requires internet"]
async fn response_structure() -> Result<()> {
let configs = [
(DirectoryKind::Doujin, "Bloom Into You"),
(DirectoryKind::Pairing, "Homura x Madoka"),
(DirectoryKind::Tag, "Aaaaaangst"),
]
.map(create_config_with_superset_view_kind)
.into_iter()
.flatten()
.collect::<Vec<_>>();
tryhard_configs(configs, |client, config| client.directory_list(config)).await?;
Ok(())
}
#[tokio::test]
#[ignore = "requires internet"]
async fn viewless_response_structure() -> Result<()> {
let configs = [
(DirectoryKind::Anthology, "And Then, To You", None),
(DirectoryKind::Author, "Nakatani Nio", None),
(DirectoryKind::Issue, "Aya Yuri Vol 11", None),
(DirectoryKind::Scanlator, "/u/ Scanlations", None),
(
DirectoryKind::Series,
"Arknights Official Comic Anthology",
None,
),
]
.map(create_config);
tryhard_configs(configs, |client, config| client.directory_list(config)).await?;
Ok(())
}
}