mango_api/requests/
mod.rs

1//! Structs and utilities for making requests to mangadex servers
2
3pub 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/// Used to deserialize errors returned from server
38#[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/// Custom error type that contains all errors that this can be emitted by this crate's functions
48#[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
72/// Type alias for the [`Result`](std::result::Result) that is used in the crate's functions
73pub type Result<T> = std::result::Result<T, Error>;
74
75/// [Entity] is implemented for all structs that represent Entity types in terms used by mangadex servers
76pub trait Entity {}
77impl<T: Entity> Entity for Vec<T> {}
78
79impl MangoClient {
80    pub const BASE_URL: &str = "https://api.mangadex.org";
81
82    /// Lowest level function that executes arbitrary [Query] and returnes its response
83    #[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    /// Deserializes responses that can be deserialized into [Entity] or a [`Vec`] of entities
98    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    /// Searches for manga with parameteres, specified by data
124    #[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    /// Essentially the same as [`search_manga`](MangoClient::search_manga) but the returned response would also contain information
136    /// about covers for each entry
137    #[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    /// Executes [`search_manga_include_cover`](MangoClient::search_manga_include_cover) and downloads cover
152    /// for each entry. Returns each search entry paired with byte respresentation of its cover
153    #[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    /// Shorthand for searching manga just by name
186    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    /// The same as [`search_manga_by_name`](MangoClient::search_manga_by_name) combined with
195    /// [`search_manga_include_cover`](MangoClient::search_manga_include_cover)
196    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    /// Queries for the feed of the manga with the given `id` and parameteres specified by `data`
205    #[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    /// Queries for the meta info about downloading chapter with the given `id`
217    #[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    /// Queries for the info about the scanlation group with the specified `id`
240    #[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    /// Queries for available tags
255    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    /// Given the name of the cover filename (on mangadex server) and the manga `id`,
269    /// downloads the needed cover art
270    #[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    /// Shorthand for deserializing server error response
287    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    /// Downloads page from the specified `url`
309    #[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    /// Queries for chunked downloading of the page from the specified `url`.
326    /// The returned [Response] can then be used to download the page chunk by chunk    
327    #[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    /// Downloads the chapter with the specified `id`, uses maximum `max_concurrent downloads` pages
341    /// downloading at each moment of time. Downloaded chapter pages are stored in the directory
342    /// "tmp/{chapter_id}"
343    #[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}