dynasty_api/
chapter.rs

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/// A configuration to get a [Chapter]
11#[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/// A wrapper around Dynasty Reader's chapter
67///
68/// # Example urls
69///
70/// - <https://dynasty-scans.com/chapters/momoiro_trance_ch01>
71/// - <https://dynasty-scans.com/chapters/liar_satsuki_can_see_death_ch54>
72#[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/// A Dynasty Reader's chapter page
85#[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}