mangadex-api

3.0.0 developpement

A lot of changes will occur in 3.0.0. Please refer to #27

Important

This git repo is just a fork from gondolyr/mangadex-api but the project and crate has been yanked so I will now maintain this crate for special-eureka and eureka-manager

The mangadex-api crate provides a convenient, high-level wrapper client for the MangaDex API, written in Rust.

It covers all public endpoints covered by their documentation.

Documentation (docs.rs)

Documentation (Project main branch)

Please note that as MangaDex is still in beta, this SDK will be subject to sudden breaking changes.

Disclaimer

mangadex-api is not affiliated with MangaDex.

Table of Contents

Requirements

Back to top

How to install

Back to top

Add mangadex-api to your dependencies:

```toml [dependencies]

...

Types and schemas are always required

mangadex-api-types-rust = "0.3.3" mangadex-api-schema-rust = "0.3.2" mangadex-api = "2.2.0" ```

If you are using cargo-edit, run

bash cargo add mangadex-api

Dependency Justification

| Dependency | Used for | Included | |:---------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|:-----------| | anyhow | Capturing unexpected errors. | always | | mangadex-api-types-rust | Enums and static data for Mangadex API | always | | mangadex-api-schema-rust | Types used for Mangadex API | always | | clap | Examples demonstrating the library's capabilities | dev builds | | derive_builder | Conveniently generating setters for the API endpoint builders. | always | | fake | Generating random data for unit tests. | dev builds | | futures | Async request processing. | always | | reqwest | Making HTTP requests to the MangaDex API. | always | | serde | Se/dese/rializing HTTP response bodies into structs. | always | | serde_json | Creating JSON objects for unit tests. | dev builds | | serde_qs | Query string serialization for HTTP requests. | always | | thiserror | Customized error handling. | always | | time | Convenience types for handing time fields. | always | | tokio | Async runtime to handle futures in (only) examples and utils feature in chapter reporting | dev builds + utils features | | url | Convenient Url type for validating and containing URLs. | always | | uuid | Convenient Uuid type for validating and containing UUIDs for requests and responses. Also used to randomly generate UUIDs for testing. | always | | wiremock | HTTP mocking to test the MangaDex API. | dev builds |

Features

Back to top

All features are not included by default. To enable them, add any of the following to your project's Cargo.toml file.

For example, to enable the multi-thread feature, add the following to your Cargo.toml file:

toml mangadex-api = { version = "2.1.0", features = ["multi-thread"] }

HTTP Client

Back to top

The mangadex_api::MangaDexClient is asynchronous, using reqwest as the HTTP client.

Response Structs

Back to top

The response structs can be found in the schemas module and contain the fields in a JSON response.

Getting Started

Back to top

This example demonstrates how to fetch a random manga.

```rust use mangadex_api::v5::MangaDexClient;

[tokio::main]

async fn main() -> anyhow::Result<()> { let client = MangaDexClient::default();

let random_manga = client
    .manga()
    .random()
    .build()?
    .send()
    .await?;

println!("{:?}", random_manga);

Ok(())

} ```

Using a custom reqwest Client

Back to top

By default, mangadex_api::MangaDexClient will use the default reqwest::Client settings.

You may provide your own reqwest::Client to customize options such as the request timeout.

```rust use reqwest::Client;

use mangadex_api::v5::MangaDexClient;

async fn run() -> anyhow::Result<()> {

let reqwestclient = Client::builder() .timeout(std::time::Duration::fromsecs(10)) .build()?;

let client = MangaDexClient::new(reqwest_client);

Ok(())

}

```

Searching manga by title

Back to top

Reference: https://api.mangadex.org/swagger.html#/Manga/get-search-manga

```rust use mangadex_api::v5::MangaDexClient;

async fn run() -> anyhow::Result<()> {

let client = MangaDexClient::default();

let manga_results = client .manga() .search() .title("full metal") .build()? .send() .await?;

println!("manga results = {:?}", manga_results);

Ok(())

}

```

Searching manga by title with reference expansion

Back to top

Every fetch will include all relationships but with minimal information such as the relationship type and ID. Reference expansion will include the full JSON object in the results for the types that are added to the request.

In the example below, any associated authors in the list of relationships will provide detailed information such as the author's name, biography, and website in the results.

References:

```rust use mangadexapi::types::{ReferenceExpansionResource, RelationshipType}; use mangadexapi::v5::schema::RelatedAttributes; use mangadex_api::v5::MangaDexClient;

async fn run() -> anyhow::Result<()> {

let client = MangaDexClient::default();

let manga_results = client .manga() .search() .title("full metal") .include(&ReferenceExpansionResource::Author) .build()? .send() .await?;

println!("manga results = {:?}", manga_results);

let authors = mangaresults.data.iter().filtermap(|manga| { for rel in &manga.relationships { if rel.type_ == RelationshipType::Author { return Some(rel); } }

None

});

for author in authors { if let Some(RelatedAttributes::Author(authorattributes)) = &author.attributes { println!("{} - {}", author.id, authorattributes.name); } }

Ok(())

}

```

Downloading chapter pages

Back to top

Reference: https://api.mangadex.org/docs/reading-chapter/

Using the old way

```rust // Imports used for downloading the pages to a file. // They are not used because we're just printing the raw bytes. // use std::fs::File; // use std::io::Write;

use reqwest::Url; use uuid::Uuid;

use mangadex_api::v5::MangaDexClient;

async fn run() -> anyhow::Result<()> {

let client = MangaDexClient::default();

let chapterid = Uuid::newv4();

let athome = client .athome() .server() .chapterid(&chapterid) .build()? .send() .await?;

let http_client = reqwest::Client::new();

// Original quality. Use .data.attributes.data_saver for smaller, compressed images. let pagefilenames = athome.chapter.data; for filename in pagefilenames { // If using the data-saver option, use "/data-saver/" instead of "/data/" in the URL. let pageurl = athome .baseurl .join(&format!( "/{qualitymode}/{chapterhash}/{pagefilename}", qualitymode = "data", chapterhash = athome.chapter.hash, page_filename = filename )) .unwrap();

let res = http_client.get(page_url).send().await?;
// The data should be streamed rather than downloading the data all at once.
let bytes = res.bytes().await?;

// This is where you would download the file but for this example,
// we're just printing the raw data.
// let mut file = File::create(&filename)?;
// let _ = file.write_all(&bytes);
println!("Chunk: {:?}", bytes);

}

Ok(())

}

```

Using the utils feature

Via (filename, bytes) vector based :

Not recommended if you want to handle each response error

```rust use crate::{utils::download::chapter::DownloadMode, MangaDexClient}; use anyhow::{Ok, Result}; /// used for file exporting use std::{ fs::{createdirall, File}, io::Write, };

/// It's from this manga called The Grim Reaper Falls In Love With A Human /// /// Chapter 1 English by Kredim

[tokio::main]

async fn main() -> Result<()> { let outputdir = "your-output-dir"; let client = MangaDexClient::default(); let chapterid = uuid::Uuid::parsestr("32b229f6-e9bf-41a0-9694-63c11191704c")?; let chapterfiles = client /// We use the download builder .download() /// Chapter id (accept uuid::Uuid) .chapter(chapterid) /// You also use DownloadMode::Normal if you want some the original quality /// /// Default : Normal .mode(DownloadMode::DataSaver) /// Enable the The MangaDex@Home report if true /// /// Default : false .report(true) /// Something that i don`t really know about /// /// More details at : https://api.mangadex.org/docs/retrieving-chapter/#basics .forceport443(false) .build()? .downloadelementvec() .await?; createdirall(format!("{}{}", outputdir, chapterid))?; for (filename, bytes) in chapterfiles { if let Some(bytes) = bytes_ { let mut file: File = File::create(format!("{}{}/{}", outputdir, chapterid, filename))?; file.write_all(&bytes)? }; } Ok(()) } ```

Via tokio-stream :

With tokio-stream, you can handle each response result

Without checker

```rust use crate::{utils::download::chapter::DownloadMode, MangaDexClient}; use anyhow::{Ok, Result}; use std::{ fs::{createdirall, File}, io::Write, }; use tokio::pin; use tokio_stream::StreamExt;

/// It's from this manga called Keiken Zumi na Kimi to, Keiken Zero na Ore ga, Otsukiai Suru Hanashi /// /// Chapter 13 English by Galaxy Degen Scans

[tokio::main]

async fn main() -> Result<()> { let outputdir = "./test-outputs/"; let client = MangaDexClient::default(); let chapterid = uuid::Uuid::parsestr("250f091f-4166-4831-9f45-89ff54bf433b")?; createdirall(format!("{}{}", outputdir, chapterid))?; let download = client /// We use the download builder .download() /// Chapter id (accept uuid::Uuid) .chapter(chapterid) /// You also use DownloadMode::Normal if you want some the original quality /// /// Default : Normal .mode(DownloadMode::DataSaver) /// Enable the The MangaDex@Home report if true /// /// Default : false .report(true) /// Something that i dont really know about /// /// More details at : https://api.mangadex.org/docs/retrieving-chapter/#basics .force_port_443(false) .build()?; let chapter_files = download.download_stream().await?; ///pin!` Required for iteration pin!(chapterfiles); while let Some((data, _, _)) = chapterfiles.next().await { let (filename, bytes) = data?; if let Some(bytes) = bytes { let mut file: File = File::create(format!("{}{}/{}", outputdir, chapterid, filename))?; file.write_all(&bytes)? }; } Ok(()) }

```

with checker

The checker is a function called after the response fetching but before retreiving the byte content. Example :

rust /// Some code here let download = client .download() .chapter(chapter_id) .mode(DownloadMode::DataSaver) .report(true) .build()?; let chapter_files = download .download_stream_with_checker(move |filename, response| { /// if this function return `true`, the current response will be skipped true }) .await?; /// Some code here too

Real example :

The checker will check return true if a file with the response content length has been created

```rust use crate::{utils::download::chapter::DownloadMode, MangaDexClient}; use anyhow::{Ok, Result}; use std::{ fs::{createdirall, File}, io::Write, }; use tokio::pin; use tokio_stream::StreamExt;

/// It's from this manga called Keiken Zumi na Kimi to, Keiken Zero na Ore ga, Otsukiai Suru Hanashi /// /// Chapter 13 English by Galaxy Degen Scans

[tokio::main]

async fn main() -> Result<()> { let outputdir = "./test-outputs/"; let client = MangaDexClient::default(); let chapterid = uuid::Uuid::parsestr("250f091f-4166-4831-9f45-89ff54bf433b")?; createdirall(format!("{}{}", outputdir, chapterid))?; let download = client .download() .chapter(chapterid) .mode(DownloadMode::DataSaver) .report(true) .build()?; let chapterfiles = download .downloadstreamwithchecker(move |filename, response| { let isskip: bool = { /// Get the response content length let contentlength = match response.contentlength() { None => return false, Some(d) => d, }; /// open the chapter image file if let core::result::Result::Ok(prefile) = File::open(format!( "{}{}/{}", outputdir, chapterid, filename.filename.clone() )) { if let core::result::Result::Ok(metadata) = prefile.metadata() { /// compare the content length and the file length metadata.len() == contentlength } else { false } } else { false } }; isskip }) .await?; pin!(chapterfiles); while let Some((data, index, len)) = chapterfiles.next().await { print!("{index} - {len} : "); if let core::result::Result::Ok(resp) = data { let (filename, bytes) = resp ; // save the bytes if the Option hase Some value if let Some(bytes) = bytes_ { let mut file: File = File::create(format!("{}{}/{}", outputdir, chapterid, filename))?; file.write_all(&bytes)?; println!("Downloaded {filename}"); }else{ println!("Skipped {filename}"); } } else if let core::result::Result::Err(resp) = data { println!("{:#?}", resp); } } Ok(()) } ```

Downloading a manga's main cover image

Back to top

Use the legacy way

While this example could directly get the cover information by passing in the cover ID, it is not often that one would have the ID off-hand, so the most common method would be from a manga result.

If you want to get all of a manga's cover images, you will need to use the cover list endpoint and use the manga[] query parameter.

```rust // Imports used for downloading the cover to a file. // They are not used because we're just printing the raw bytes. // use std::fs::File; // use std::io::Write;

use reqwest::Url; use uuid::Uuid;

use mangadexapi::types::RelationshipType; use mangadexapi::v5::MangaDexClient; use mangadexapi::CDNURL;

async fn run() -> anyhow::Result<()> {

let client = MangaDexClient::default();

let mangaid = Uuid::newv4(); let manga = client .manga() .get() .mangaid(&mangaid) .build()? .send() .await?;

let coverid = manga .data .relationships .iter() .find(|related| related.type == RelationshipType::CoverArt) .expect("no cover art found for manga") .id; let cover = client .cover() .get() .coverid(&coverid) .build()? .send() .await?;

// This uses the best quality image. // To use smaller, thumbnail-sized images, append any of the following: // // - .512.jpg // - .256.jpg // // For example, "https://uploads.mangadex.org/covers/8f3e1818-a015-491d-bd81-3addc4d7d56a/4113e972-d228-4172-a885-cb30baffff97.jpg.512.jpg" let coverurl = Url::parse(&format!( "{}/covers/{}/{}", CDNURL, mangaid, cover.data.attributes.filename )) .unwrap();

let http_client = reqwest::Client::new();

let res = httpclient.get(coverurl).send().await?; // The data should be streamed rather than downloading the data all at once. let bytes = res.bytes().await?;

// This is where you would download the file but for this example, we're just printing the raw data. // let mut file = File::create(&filename)?; // let _ = file.write_all(&bytes); println!("Chunk: {:?}", bytes);

Ok(())

}

```

Using the utils feature

via a cover id

```rust use anyhow::Result; use uuid::Uuid; use crate::MangaDexClient; use std::{io::Write, fs::File};

/// Download the volume 2 cover of [Lycoris Recoil](https://mangadex.org/title/9c21fbcd-e22e-4e6d-8258-7d580df9fc45/lycoris-recoil)
#[tokio::main]
async fn main() -> Result<()>{
    let cover_id : Uuid = Uuid::parse_str("0bc12ff4-3cec-4244-8582-965b8be496ea")?;
    let client : MangaDexClient = MangaDexClient::default();
    let (filename, bytes) = client.download().cover().build()?.via_cover_id(cover_id).await?;
    let mut file = File::create(format!("{}/{}", "your-output-dir", filename))?;
    file.write_all(&bytes)?;
    Ok(())
}

```

via a manga id

```rust use anyhow::Result; use uuid::Uuid; use crate::MangaDexClient; use std::{io::Write, fs::File};

/// Download the [Kimi tte Watashi no Koto Suki Nandesho?](https://mangadex.org/title/f75c2845-0241-4e69-87c7-b93575b532dd/kimi-tte-watashi-no-koto-suki-nandesho) cover
/// 
/// For test... of course :3
#[tokio::main]
async fn main() -> Result<()>{
    let manga_id : Uuid = Uuid::parse_str("f75c2845-0241-4e69-87c7-b93575b532dd")?;
    let client : MangaDexClient = MangaDexClient::default();
    let (filename, bytes) = client
        .download()
        .cover()
        /// you can use
        /// 
        /// ```rust
        /// .quality(CoverQuality::Size512)
        /// ``` for 512
        /// or
        /// ```rust
        /// .quality(CoverQuality::Size256)
        /// ``` for 256
        .build()?.via_manga_id(manga_id).await?;
    let mut file = File::create(format!("{}/{}", "test-outputs/covers", filename))?;
    file.write_all(&bytes)?;
    Ok(())
}

```

Changelog

Back to top

The changelog can be found here.

Changes are added manually to keep the changelog human-readable with summaries of the changes from each version.

License

Back to top

Licensed under either of

at your option.

Contribution

Back to top

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Contributing

Back to top

We welcome contributions from everyone. There are many ways to contribute and the CONTRIBUTING.md document explains how you can contribute and get started.