1use serde::{Deserialize, Serialize};
2
3use crate::{
4 directory_list::DirectoryListChapterItem, recent_chapter::RecentChapterItem, tag::TagItem,
5 DynastyReaderRoute, DYNASTY_READER_BASE,
6};
7
8use self::utils::chapter_name_to_permalink;
9
10#[allow(missing_docs)]
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ChapterConfig {
14 pub name: String,
15}
16
17impl From<String> for ChapterConfig {
18 fn from(s: String) -> Self {
19 ChapterConfig { name: s }
20 }
21}
22
23impl From<DirectoryListChapterItem> for ChapterConfig {
24 fn from(item: DirectoryListChapterItem) -> Self {
25 ChapterConfig { name: item.title }
26 }
27}
28
29impl From<RecentChapterItem> for ChapterConfig {
30 fn from(item: RecentChapterItem) -> Self {
31 ChapterConfig { name: item.title }
32 }
33}
34
35#[cfg(feature = "search")]
36impl TryFrom<crate::search::SearchItem> for ChapterConfig {
37 type Error = anyhow::Error;
38
39 fn try_from(value: crate::search::SearchItem) -> Result<Self, Self::Error> {
40 if matches!(value.kind, crate::search::SearchCategory::Chapter) {
41 Ok(ChapterConfig { name: value.title })
42 } else {
43 Err(anyhow::anyhow!("this search item is not a chapter"))
44 }
45 }
46}
47
48impl DynastyReaderRoute for ChapterConfig {
49 fn request_builder(
50 &self,
51 client: &reqwest::Client,
52 url: reqwest::Url,
53 ) -> reqwest::RequestBuilder {
54 client.get(url)
55 }
56
57 fn request_url(&self) -> reqwest::Url {
58 let permalink = chapter_name_to_permalink(&self.name);
59
60 DYNASTY_READER_BASE
61 .join(&format!("chapters/{}.json", permalink))
62 .unwrap()
63 }
64}
65
66#[allow(missing_docs)]
73#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
74pub struct Chapter {
75 pub title: String,
76 pub long_title: String,
77 pub permalink: String,
78 pub released_on: String,
79 pub added_on: String,
80 pub tags: Vec<TagItem>,
81 pub pages: Vec<ChapterPage>,
82}
83
84#[allow(missing_docs)]
86#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
87pub struct ChapterPage {
88 pub name: String,
89 #[serde(deserialize_with = "crate::utils::join_path_with_dynasty_reader_base")]
90 pub url: String,
91}
92
93mod utils {
94 use lazy_regex::regex;
95
96 use crate::utils::name_to_permalink;
97
98 pub(super) fn chapter_name_to_permalink(chapter_name: &str) -> String {
99 let chapter_re = regex!(r"(ch[\d.]+):");
100
101 let name = chapter_re
102 .find(chapter_name)
103 .map(|matches| &chapter_name[..matches.end()])
104 .unwrap_or(chapter_name);
105
106 name_to_permalink(name)
107 }
108
109 #[cfg(test)]
110 mod tests {
111 use super::*;
112
113 #[test]
114 fn should_convert_chapter_name_to_permalink() {
115 let predicates = [
116 ("\"Toda-san,\"", "toda_san"),
117 ("B.G.M.R.S.P.", "b_g_m_r_s_p"),
118 ("Assorted NicoMaki (03015)", "assorted_nicomaki_03015"),
119 (
120 "Adachi and Shimamura (Moke ver.) ch27.1: Leaving Azure",
121 "adachi_and_shimamura_moke_ver_ch27_1",
122 ),
123 (
124 "Love Live! Comic Anthology μ’s Precious Days ch01",
125 "love_live_comic_anthology_μs_precious_days_ch01",
126 ),
127 (
128 "a_story_about_doing_xx_to_girls_from_different_species_ch51",
129 "a_story_about_doing_xx_to_girls_from_different_species_ch51",
130 ),
131 ];
132
133 for (left, right) in predicates {
134 assert_eq!(chapter_name_to_permalink(left), right)
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use anyhow::Result;
143
144 use crate::test_utils::tryhard_configs;
145
146 use super::*;
147
148 fn create_config(c: &str) -> ChapterConfig {
149 c.to_string().into()
150 }
151
152 #[tokio::test]
153 #[ignore = "requires internet"]
154 async fn response_structure() -> Result<()> {
155 let configs = [
156 "4-Koma Starlight ch01",
157 "4-Koma Starlight ch01: Act 1: Nice To Meet You!",
158 "4_koma_starlight_ch01",
159 "Just ChisaTaki Kissing",
160 "just_chisataki_kissing",
161 ]
162 .map(create_config);
163
164 tryhard_configs(configs, |client, config| client.chapter(config)).await?;
165
166 Ok(())
167 }
168}