mango_api/
lib.rs

1//! Core crate for mango which provides interface for server interaction as well as
2//! as the implementation of an asynchronous chapter donwloader.
3//!
4//! It is recommended to look through the [mangadex API documentation](<https://api.mangadex.org/docs>)
5//! as it can clarify how to use this library.
6//!
7//! For starters, refer to the [MangoClient] struct that provides most of the utilities and functions.
8//! Structs that represent query parameteres from manga, chapter, etc. can be found inside [requests] module
9//! in the respectful modules.
10
11// TODO: add timeout to open_page
12// TODO: test error handling
13// TODO: test the program with model when instead sharing downloadings buffer across tasks,
14// this buffer is only held by manager and open_page only gets page information after submitting
15// request to manager
16
17pub mod requests;
18pub mod viewer;
19
20use requests::Result;
21use reqwest::Client;
22use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
23use reqwest_tracing::TracingMiddleware;
24
25#[doc(hidden)]
26pub use viewer::ChapterViewer;
27
28/// Entry point for most of the crate interactions
29///
30/// Provides functions for interacting with the mangadex servers as well as
31/// the access to the [`ChapterViewer`] struct
32#[derive(Clone, Debug)]
33pub struct MangoClient {
34    client: ClientWithMiddleware,
35}
36
37impl MangoClient {
38    /// Creates new instance of [MangoClient]
39    pub fn new() -> Result<Self> {
40        let res = Client::builder().user_agent("Mango/1.0").build()?;
41        let res = ClientBuilder::new(res)
42            .with(TracingMiddleware::default())
43            .build();
44
45        Ok(Self { client: res })
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use requests::{manga::*, query_utils::*, *};
53
54    use tracing_subscriber::filter::EnvFilter;
55    use tracing_subscriber::filter::LevelFilter;
56    use tracing_subscriber::layer::SubscriberExt as _;
57    use tracing_subscriber::prelude::*;
58
59    use std::collections::HashMap;
60
61    use tokio::io::AsyncWriteExt as _;
62
63    #[tokio::test]
64    async fn test_search_manga() {
65        let client = MangoClient::new().unwrap();
66        let query = MangaQuery::builder()
67            .title("Chainsaw man")
68            // .status(vec![MangaStatus::Ongoing])
69            // .original_language(vec![Locale::En])
70            .build();
71        let resp = client.search_manga(&query).await.unwrap();
72
73        let mut out = tokio::fs::File::create("test_files/manga_struct")
74            .await
75            .unwrap();
76
77        out.write_all(format!("{resp:#?}").as_bytes())
78            .await
79            .unwrap();
80    }
81
82    #[tokio::test]
83    async fn test_get_manga_feed() {
84        let client = MangoClient::new().unwrap();
85
86        let query = MangaQuery::builder()
87            .title("Chainsaw Man")
88            .available_translated_language(vec![Locale::En])
89            .build();
90
91        let chainsaw_manga_id = client.search_manga(&query).await.unwrap()[0].id.clone();
92
93        let mut query_sorting_options = HashMap::new();
94
95        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
96
97        let query_data = MangaFeedQuery::builder()
98            .translated_language(vec![Locale::En])
99            .order(query_sorting_options)
100            .build();
101
102        let chapters = client
103            .get_manga_feed(&chainsaw_manga_id, &query_data)
104            .await
105            .unwrap();
106
107        let mut out = tokio::fs::File::create("test_files/chapters_struct")
108            .await
109            .unwrap();
110
111        out.write_all(format!("{chapters:#?}").as_bytes())
112            .await
113            .unwrap();
114    }
115
116    #[tokio::test]
117    async fn test_chapter_download() {
118        let client = MangoClient::new().unwrap();
119
120        let query = MangaQuery::builder()
121            .title("Chainsaw man")
122            .available_translated_language(vec![Locale::En])
123            .build();
124
125        let chainsaw_manga_id = client.search_manga(&query).await.unwrap()[0].id.clone();
126
127        let mut query_sorting_options = HashMap::new();
128        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
129
130        let query_data = MangaFeedQuery::builder()
131            .translated_language(vec![Locale::En])
132            .order(query_sorting_options)
133            .build();
134
135        let chapters = client
136            .get_manga_feed(&chainsaw_manga_id, &query_data)
137            .await
138            .unwrap();
139
140        let mut out = tokio::fs::File::create("test_files/chapters_meta")
141            .await
142            .unwrap();
143
144        let id = chapters[2].id.clone();
145
146        let download_meta = client.get_chapter_download_meta(&id).await.unwrap();
147
148        out.write_all(format!("{download_meta:#?}\n").as_bytes())
149            .await
150            .unwrap();
151
152        let base_url = format!(
153            "{}/data/{}",
154            download_meta.base_url, download_meta.chapter.hash
155        );
156
157        if !std::fs::exists("test_files/pages").unwrap() {
158            std::fs::create_dir("test_files/pages").unwrap();
159        }
160        for (i, page_url) in download_meta.chapter.data.into_iter().enumerate().take(3) {
161            let url = format!("{base_url}/{page_url}");
162
163            let bytes = client.download_full_page(&url).await.unwrap();
164
165            let mut out_page = tokio::fs::File::create(format!("test_files/pages/{i}.png"))
166                .await
167                .unwrap();
168
169            out_page.write_all(&bytes).await.unwrap();
170        }
171    }
172
173    #[tokio::test]
174    async fn test_get_scanlation_group() {
175        let client = MangoClient::new().unwrap();
176
177        let query = MangaQuery::builder()
178            .title("Chainsaw man")
179            .available_translated_language(vec![Locale::En])
180            .build();
181
182        let chainsaw_manga_id = client.search_manga(&query).await.unwrap()[0].id.clone();
183
184        let mut query_sorting_options = HashMap::new();
185
186        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
187
188        let query_data = MangaFeedQuery::builder()
189            .translated_language(vec![Locale::En])
190            .order(query_sorting_options)
191            .build();
192
193        let chapters = client
194            .get_manga_feed(&chainsaw_manga_id, &query_data)
195            .await
196            .unwrap();
197
198        let chapter_relatioships = chapters[2].relationships.clone();
199
200        let mut scanlation_group_id = None;
201        for relationship in chapter_relatioships {
202            match relationship.entity_type {
203                EntityType::ScanlationGroup => {
204                    scanlation_group_id = Some(relationship.id);
205
206                    break;
207                }
208                _ => {}
209            }
210        }
211
212        let scanlation_group_id = scanlation_group_id.unwrap();
213
214        let scanlation_group = client
215            .get_scanlation_group(&scanlation_group_id)
216            .await
217            .unwrap();
218
219        let scanlation_group_name = scanlation_group.attributes.name;
220
221        let mut out = tokio::fs::File::create("test_files/scanlation_group_name_test")
222            .await
223            .unwrap();
224
225        out.write_all(format!("Scanlation group name: {scanlation_group_name}").as_bytes())
226            .await
227            .unwrap();
228    }
229
230    #[tokio::test]
231    async fn test_pageness() {
232        let client = MangoClient::new().unwrap();
233
234        let query = MangaQuery::builder()
235            .title("Chainsaw man")
236            .available_translated_language(vec![Locale::En])
237            .build();
238
239        let chainsaw_manga_id = client.search_manga(&query).await.unwrap()[0].id.clone();
240
241        let mut query_sorting_options = HashMap::new();
242
243        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
244
245        let query_data = MangaFeedQuery::builder()
246            .translated_language(vec![Locale::En])
247            .order(query_sorting_options)
248            .limit(200)
249            .offset(1)
250            .excluded_groups(vec![
251                "4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb".to_owned(),
252                "a38fc704-90ab-452f-9336-59d84997a9ce".to_owned(),
253            ])
254            .build();
255
256        let chapters = client
257            .get_manga_feed(&chainsaw_manga_id, &query_data)
258            .await
259            .unwrap();
260
261        let mut out = tokio::fs::File::create("test_files/test_pages")
262            .await
263            .unwrap();
264
265        out.write_all(format!("{chapters:#?}").as_bytes())
266            .await
267            .unwrap();
268    }
269
270    #[tokio::test]
271    async fn test_viewer() {
272        {
273            std::fs::File::create("logs").unwrap();
274        }
275
276        let filter = EnvFilter::builder()
277            .with_default_directive(LevelFilter::TRACE.into())
278            .from_env_lossy();
279
280        let (_writer, _guard) = tracing_appender::non_blocking(
281            std::fs::File::options().append(true).open("logs").unwrap(),
282        );
283
284        tracing_subscriber::registry()
285            .with(
286                tracing_subscriber::fmt::layer()
287                    .with_test_writer()
288                    .with_writer(_writer)
289                    .pretty()
290                    .compact(),
291            )
292            .with(filter)
293            .init();
294
295        let client = MangoClient::new().unwrap();
296
297        let chainsaw_manga_id = client
298            .search_manga(&MangaQuery {
299                title: Some("Chainsaw Man".to_string()),
300                available_translated_language: Some(vec![Locale::En]),
301                ..Default::default()
302            })
303            .await
304            .unwrap()[0]
305            .id
306            .clone();
307
308        let mut query_sorting_options = HashMap::new();
309
310        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
311
312        let query_data = MangaFeedQuery::builder()
313            .translated_language(vec![Locale::En])
314            .order(query_sorting_options)
315            .build();
316
317        let chapters = client
318            .get_manga_feed(&chainsaw_manga_id, &query_data)
319            .await
320            .unwrap();
321
322        let first_chapter = chapters[2].clone();
323
324        let mut viewer = client.chapter_viewer(&first_chapter.id, 8).await.unwrap();
325
326        let chapter_len = first_chapter.attributes.pages;
327
328        let mut page_paths = Vec::new();
329        for i in 0..chapter_len {
330            page_paths.push(viewer.open_page(i + 1).await);
331        }
332
333        let mut out = tokio::fs::File::create("test_files/page_paths")
334            .await
335            .unwrap();
336
337        out.write(format!("{page_paths:#?}").as_bytes())
338            .await
339            .unwrap();
340    }
341
342    #[tokio::test]
343    async fn test_download_full_chapter() {
344        let client = MangoClient::new().unwrap();
345
346        let query = MangaQuery::builder()
347            .title("Chainsaw man")
348            .available_translated_language(vec![Locale::En])
349            .build();
350
351        let chainsaw_manga_id = client.search_manga(&query).await.unwrap()[0].id.clone();
352
353        let mut query_sorting_options = HashMap::new();
354
355        query_sorting_options.insert(OrderOption::Chapter, Order::Asc);
356
357        let query_data = MangaFeedQuery::builder()
358            .translated_language(vec![Locale::En])
359            .order(query_sorting_options)
360            .build();
361
362        let chapters = client
363            .get_manga_feed(&chainsaw_manga_id, &query_data)
364            .await
365            .unwrap();
366
367        let second_chapter = chapters[4].clone();
368
369        let chapter_path = client
370            .download_full_chapter(&second_chapter.id, 8)
371            .await
372            .unwrap();
373
374        let mut out = tokio::fs::File::create("test_files/chapter_path_test")
375            .await
376            .unwrap();
377
378        out.write(format!("{chapter_path:#?}").as_bytes())
379            .await
380            .unwrap();
381    }
382
383    #[tokio::test]
384    async fn test_get_manga_include_cover() {
385        let client = MangoClient::new().unwrap();
386        let query = MangaQuery::builder()
387            .title("Chainsaw man")
388            // .status(vec![MangaStatus::Ongoing])
389            // .original_language(vec![Locale::En])
390            .build();
391
392        let resp = client.search_manga_include_cover(&query).await.unwrap();
393
394        let manga = resp[0].clone();
395
396        let mut cover = None;
397
398        for relation in manga.relationships {
399            match relation.entity_type {
400                EntityType::CoverArt => {
401                    cover = Some(serde_json::from_value::<cover::CoverArtAttributes>(
402                        relation.attributes.unwrap(),
403                    ));
404                }
405                _ => {}
406            }
407        }
408
409        let cover = cover.unwrap().unwrap();
410
411        let cover = client
412            .download_full_cover(&manga.id, &cover.file_name)
413            .await
414            .unwrap();
415
416        let mut out_cover = tokio::fs::File::create("test_files/cover.png")
417            .await
418            .unwrap();
419        out_cover.write_all(cover.as_ref()).await.unwrap();
420
421        let mut out = tokio::fs::File::create("test_files/manga_feed_with_cover_test")
422            .await
423            .unwrap();
424
425        out.write_all(format!("{resp:#?}").as_bytes())
426            .await
427            .unwrap();
428    }
429
430    #[tokio::test]
431    async fn test_get_manga_with_cover() {
432        let client = MangoClient::new().unwrap();
433
434        let query = MangaQuery::builder()
435            .title("Chainsaw man")
436            // .status(vec![MangaStatus::Ongoing])
437            // .original_language(vec![Locale::En])
438            .build();
439
440        let resp = client.search_manga_with_cover(&query).await.unwrap();
441
442        let (chainsaw_manga, chainsaw_cover) = resp[0].clone();
443
444        let mut out_manga = tokio::fs::File::create("test_files/manga_with_cover")
445            .await
446            .unwrap();
447
448        let mut out_manga_cover = tokio::fs::File::create("test_files/maga_with_cover_cover.png")
449            .await
450            .unwrap();
451
452        out_manga
453            .write_all(format!("{chainsaw_manga:#?}").as_bytes())
454            .await
455            .unwrap();
456
457        out_manga_cover
458            .write_all(chainsaw_cover.as_ref())
459            .await
460            .unwrap();
461    }
462
463    #[tokio::test]
464    async fn test_get_tags() {
465        let client = MangoClient::new().unwrap();
466        let resp = client.get_tags().await.unwrap();
467
468        let mut out_tags = tokio::fs::File::create("test_files/tags").await.unwrap();
469
470        out_tags
471            .write_all(format!("{resp:#?}").as_bytes())
472            .await
473            .unwrap();
474    }
475}