1pub 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#[derive(Clone, Debug)]
33pub struct MangoClient {
34 client: ClientWithMiddleware,
35}
36
37impl MangoClient {
38 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 .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 .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 .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}