1pub mod chapter;
4pub mod cover;
5pub mod manga;
6pub mod query_utils;
7pub mod scanlation_group;
8pub mod tag;
9
10use crate::viewer::PageStatus;
11use crate::MangoClient;
12use chapter::{Chapter, ChapterDownloadMeta};
13use cover::CoverArtAttributes;
14use manga::{Manga, MangaFeedQuery, MangaQuery};
15use query_utils::{EmptyQuery, EntityType, Locale, Query, ResponseResultOk as _};
16use scanlation_group::ScanlationGroup;
17use tag::Tag;
18
19use bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use thiserror::Error;
23
24use reqwest::{Response, StatusCode};
25
26use tokio::sync::mpsc;
27use tokio::task;
28use tokio_stream::wrappers::ReceiverStream;
29use tracing::instrument::Instrument as _;
30
31use std::default::Default;
32use std::path::PathBuf;
33use std::sync::Arc;
34
35use parking_lot::Mutex;
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
39pub struct ServerResponseError {
40 id: String,
41 status: i32,
42 title: String,
43 detail: Option<String>,
44 context: Option<String>,
45}
46
47#[derive(Error, Debug)]
49pub enum Error {
50 #[error(transparent)]
51 ReqwestError(#[from] reqwest::Error),
52 #[error(transparent)]
53 RequestWithMiddleWareError(#[from] reqwest_middleware::Error),
54 #[error(transparent)]
55 JsonError(#[from] serde_json::Error),
56 #[error("error while parsing json value")]
57 ParseError,
58 #[error("104 server response")]
59 ConnectionResetByPeerError(Vec<ServerResponseError>),
60 #[error("400 server response")]
61 BadRequestError(Vec<ServerResponseError>),
62 #[error("404 server response")]
63 NotFoundError(Vec<ServerResponseError>),
64 #[error("403 server respose")]
65 ForbiddenError(Vec<ServerResponseError>),
66 #[error(transparent)]
67 QsError(#[from] serde_qs::Error),
68 #[error(transparent)]
69 IoError(#[from] std::io::Error),
70}
71
72pub type Result<T> = std::result::Result<T, Error>;
74
75pub trait Entity {}
77impl<T: Entity> Entity for Vec<T> {}
78
79impl MangoClient {
80 pub const BASE_URL: &str = "https://api.mangadex.org";
81
82 #[tracing::instrument]
84 pub async fn query(&self, base_url: &str, query: &impl Query) -> Result<Response> {
85 let query_data = match serde_qs::to_string(query) {
86 Ok(res) => res,
87 Err(e) => return Err(Error::QsError(e)),
88 };
89
90 let url = format!("{base_url}?{query_data}");
91 match self.client.get(url).send().await {
92 Ok(res) => Ok(res),
93 Err(e) => Err(Error::RequestWithMiddleWareError(e)),
94 }
95 }
96
97 pub async fn parse_respond_data<T>(mut resp: Value) -> Result<T>
99 where
100 for<'a> T: Entity + Deserialize<'a> + Serialize,
101 {
102 let responded_without_errors = resp.response_result_ok()?;
103
104 if responded_without_errors {
105 let data = match resp.get_mut("data") {
106 Some(d) => d,
107 None => return Err(Error::ParseError),
108 };
109
110 Ok(serde_json::from_value::<T>(data.take())?)
111 } else {
112 let errors = match resp.get_mut("errors") {
113 Some(d) => d,
114 None => return Err(Error::ParseError),
115 };
116
117 let err: Vec<ServerResponseError> = serde_json::from_value(errors.take())?;
118
119 Err(Error::BadRequestError(err))
120 }
121 }
122
123 #[tracing::instrument]
125 pub async fn search_manga(&self, data: &MangaQuery) -> Result<Vec<Manga>> {
126 let resp: Value = self
127 .query(&format!("{}/manga", MangoClient::BASE_URL), data)
128 .await?
129 .json()
130 .await?;
131
132 MangoClient::parse_respond_data(resp).await
133 }
134
135 #[tracing::instrument]
138 pub async fn search_manga_include_cover(&self, data: &MangaQuery) -> Result<Vec<Manga>> {
139 let mut data = data.clone();
140 data.includes = Some(serde_json::json!(["cover_art"]));
141
142 let resp: Value = self
143 .query(&format!("{}/manga", MangoClient::BASE_URL), &data)
144 .await?
145 .json()
146 .await?;
147
148 MangoClient::parse_respond_data(resp).await
149 }
150
151 #[tracing::instrument]
154 pub async fn search_manga_with_cover(&self, data: &MangaQuery) -> Result<Vec<(Manga, Bytes)>> {
155 let resp = self.search_manga_include_cover(data).await?;
156
157 let mut res = Vec::new();
158 for manga in resp {
159 let mut cover = None;
160 for relation in manga.relationships.iter() {
161 if let EntityType::CoverArt = relation.entity_type {
162 cover = Some(serde_json::from_value::<CoverArtAttributes>(
163 relation
164 .clone()
165 .attributes
166 .expect("didn't get relation attributes from server"),
167 )?);
168
169 break;
170 }
171 }
172
173 let cover = cover.expect("server didn't send the required cover art info");
174
175 let bytes = self
176 .download_full_cover(&manga.id, &cover.file_name)
177 .await?;
178
179 res.push((manga, bytes));
180 }
181
182 Ok(res)
183 }
184
185 pub async fn search_manga_by_name(&self, name: &str) -> Result<Vec<Manga>> {
187 self.search_manga(&MangaQuery {
188 title: Some(name.to_string()),
189 ..Default::default()
190 })
191 .await
192 }
193
194 pub async fn search_manga_by_name_include_cover(&self, name: &str) -> Result<Vec<Manga>> {
197 self.search_manga_include_cover(&MangaQuery {
198 title: Some(name.to_string()),
199 ..Default::default()
200 })
201 .await
202 }
203
204 #[tracing::instrument]
206 pub async fn get_manga_feed(&self, id: &str, data: &MangaFeedQuery) -> Result<Vec<Chapter>> {
207 let resp: Value = self
208 .query(&format!("{}/manga/{id}/feed", MangoClient::BASE_URL), data)
209 .await?
210 .json()
211 .await?;
212
213 MangoClient::parse_respond_data(resp).await
214 }
215
216 #[tracing::instrument]
218 pub async fn get_chapter_download_meta(&self, id: &str) -> Result<ChapterDownloadMeta> {
219 let mut resp: Value = self
220 .query(
221 &format!("{}/at-home/server/{id}", MangoClient::BASE_URL),
222 &EmptyQuery {},
223 )
224 .await?
225 .json()
226 .await?;
227
228 let responded_without_errors = resp.response_result_ok()?;
229
230 if responded_without_errors {
231 Ok(serde_json::from_value(resp)?)
232 } else {
233 Err(Error::BadRequestError(serde_json::from_value::<
234 Vec<ServerResponseError>,
235 >(resp["errors"].take())?))
236 }
237 }
238
239 #[tracing::instrument(skip(self))]
241 pub async fn get_scanlation_group(&self, id: &str) -> Result<ScanlationGroup> {
242 let resp: Value = self
243 .query(
244 &format!("{}/group/{id}", MangoClient::BASE_URL),
245 &EmptyQuery {},
246 )
247 .await?
248 .json()
249 .await?;
250
251 MangoClient::parse_respond_data(resp).await
252 }
253
254 pub async fn get_tags(&self) -> Result<Vec<Tag>> {
256 let resp: Value = self
257 .query(
258 &format!("{}/manga/tag", MangoClient::BASE_URL),
259 &EmptyQuery {},
260 )
261 .await?
262 .json()
263 .await?;
264
265 MangoClient::parse_respond_data(resp).await
266 }
267
268 #[tracing::instrument(skip(self))]
271 pub async fn download_full_cover(&self, manga_id: &str, cover_filename: &str) -> Result<Bytes> {
272 let url = format!("https://uploads.mangadex.org/covers/{manga_id}/{cover_filename}");
273
274 let resp = self.query(&url, &EmptyQuery {}).await?;
275 let status = resp.status();
276 if status != 200 {
277 tracing::warn!("got an error from server: {resp:#?}");
278 }
279
280 match resp.bytes().await {
281 Ok(res) => Ok(res),
282 Err(e) => Err(Error::ReqwestError(e)),
283 }
284 }
285
286 async fn deserialize_reponse_error<T: std::fmt::Debug>(resp: Response) -> Result<T> {
288 let status = resp.status();
289
290 let errors = resp.json::<Value>().await?["errors"].take();
291 let e: Vec<ServerResponseError> = serde_json::from_value(errors)?;
292
293 let res = match status.as_str() {
294 "400" => Err(Error::BadRequestError(e)),
295 "403" => Err(Error::ForbiddenError(e)),
296 "404" => Err(Error::NotFoundError(e)),
297 "104" => Err(Error::ConnectionResetByPeerError(e)),
298 _ => {
299 unreachable!()
300 }
301 };
302
303 tracing::warn!("got {res:#?} from server");
304
305 res
306 }
307
308 #[tracing::instrument]
310 pub async fn download_full_page(&self, url: &str) -> Result<Bytes> {
311 let resp = self.query(url, &EmptyQuery {}).await?;
312
313 if resp.status() != StatusCode::OK {
314 Self::deserialize_reponse_error(resp)
315 .in_current_span()
316 .await
317 } else {
318 match resp.bytes().await {
319 Ok(res) => Ok(res),
320 Err(e) => Err(Error::ReqwestError(e)),
321 }
322 }
323 }
324
325 #[tracing::instrument]
328 pub async fn get_page_chunks(&self, url: &str) -> Result<Response> {
329 let resp = self.query(url, &EmptyQuery {}).await?;
330
331 if resp.status() != StatusCode::OK {
332 Self::deserialize_reponse_error(resp)
333 .in_current_span()
334 .await
335 } else {
336 Ok(resp)
337 }
338 }
339
340 #[tracing::instrument(skip(self))]
344 pub async fn download_full_chapter(
345 &self,
346 chapter_id: &str,
347 mut max_concurrent_downloads: usize,
348 ) -> Result<PathBuf> {
349 max_concurrent_downloads = max_concurrent_downloads.max(1);
350
351 let download_meta = self
352 .get_chapter_download_meta(chapter_id)
353 .in_current_span()
354 .await?;
355
356 let chapter_size = download_meta.chapter.data.len();
357 let buf = Arc::new(Mutex::new(vec![PageStatus::Idle; chapter_size]));
358
359 let (manager_command_sender, manager_command_receiver) = mpsc::channel(10);
360 let manager_command_receiver = ReceiverStream::new(manager_command_receiver);
361
362 let (downloadings_spawner_command_sender, downloadings_spawner_command_receiver) =
363 mpsc::channel(10);
364 let downloadings_spawner_command_receiver =
365 ReceiverStream::new(downloadings_spawner_command_receiver);
366
367 let res = Ok(format!("tmp/{}", &download_meta.chapter.hash).into());
368
369 let chapter_download_dir = format!("tmp/{}", &download_meta.chapter.hash);
370 if !std::fs::exists(&chapter_download_dir).expect("failed to get info about tmp directory")
371 {
372 std::fs::create_dir_all(&chapter_download_dir)
373 .expect("failed to create directory for storing pages");
374 } else {
375 let dir_meta = std::fs::metadata(&chapter_download_dir)
376 .expect("failed to create directory for storing pages");
377
378 if !dir_meta.is_dir() {
379 panic!("failed to create directory for storing pages: file already exists, not a directory");
380 }
381 }
382
383 let manager = Self::downloadings_manager()
384 .opened_page(1)
385 .statuses(Arc::clone(&buf))
386 .spawner_command_sender(downloadings_spawner_command_sender.clone())
387 .command_receiver(manager_command_receiver)
388 .max_concurrent_downloads(max_concurrent_downloads)
389 .chapter_size(chapter_size)
390 .call();
391
392 let manager = task::spawn(manager);
393
394 let spawner = Self::downloadings_spawner()
395 .client(self.clone())
396 .meta(download_meta)
397 .command_receiver(downloadings_spawner_command_receiver)
398 .manager_command_sender(manager_command_sender)
399 .statuses(buf)
400 .call();
401
402 task::spawn(spawner);
403
404 if let Err(e) = manager.await {
405 tracing::warn!("downloadings manager finished with error: {e:#?}");
406 }
407
408 res
409 }
410}