Crates.io | mangadex-api |
lib.rs | mangadex-api |
version | 3.4.1 |
source | src |
created_at | 2020-12-06 21:07:55.536993 |
updated_at | 2024-11-20 17:55:21.150133 |
description | SDK for the MangaDex API |
homepage | |
repository | https://github.com/tonymushah/mangadex-api |
max_upload_size | |
id | 320252 |
size | 1,193,741 |
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 (Old repo main
branch)
Please note that as MangaDex is still in beta, this SDK will be subject to sudden breaking changes (a lot).
mangadex-api
is not affiliated with MangaDex.
oauth
feature)
mangadex-api
is the SDK itselfmangadex-api-types
is the crate containing all enums, and static data. Those are non_exhaustive
by defaultmangadex-api-schema
contains all the response structs required. These support serialization with the serialize
featuremangadex-api-inputs-types
contains input types for endpoint. Please note that this input crate only contain those with multiple parameters.Add mangadex-api
to your dependencies:
[dependencies]
# ...
# Types and schemas are always required
mangadex-api-types-rust = "0.10"
mangadex-api-schema-rust = "0.10"
mangadex-api = "3.4"
If you are using cargo-edit
, run
cargo add mangadex-api
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, tokio-mutli-thread , and rw-mutli-thread |
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 |
All features are not included by default. To enable them, add any of the following to your project's Cargo.toml
file.
multi-thread
Enable the MangaDexClient
to be thread-safe, at the cost of operations being slightly more expensive.
legacy-auth
Deprecated
Enable the usage of the < 5.9.0
login system in the SDK. Please visit the Mangadex Discord for more details
utils
Enable the usage of the MangaDexClient::download()
. Allows you to download chapters or covers image without tears and long code.
tokio-multi-thread
Enable the usage of tokio::sync::Mutex
, instead of futures::lock::Mutex
rw-mutli-thread
Enable the usage of tokio::sync::RwLock
, instead of futures::lock::Mutex
in the client.
It can be useful if you want a flexible concurent mutli-thread.
legacy-user-delete
Deprecated
Enable the usage of the user delete endpoints.
oauth
(Enabled by default)
Enable the use of the brand new OAuth 2.0 login introduced in MangaDex API 5.9.0.
Quick Note: This oauth
feature use the personal-client approach which means that you need to register a personal client and wait that it'll be validated.
More details here here
custom_list_v2
: Enable the usage of the upcoming custom list system. Please note that these endpoints are deployed yet on api.mangadex.org
but you can use them on api.mangadex.dev
(their live dev API). For more information, please refer to Follows/CustomList API Changelog - BREAKING CHANGES
on the MangaDex Forums
For example, to enable the multi-thread
feature, add the following to your Cargo.toml
file:
mangadex-api = { version = "3.2.0", features = ["multi-thread"] }
The mangadex_api::MangaDexClient
is asynchronous, using
reqwest
as the HTTP client.
The response structs can be found in the mangadex-api-schema-rust
crate and contain the fields in a JSON response.
This example demonstrates how to fetch a random manga.
use mangadex_api::v5::MangaDexClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
let random_manga = client
.manga()
.random()
.get()
.send()
.await?;
println!("{:?}", random_manga);
Ok(())
}
This example demonstates how to fetch the Mangadex Popular Titles
use mangadex_api::MangaDexClient;
use mangadex_api_schema_rust::v5::RelatedAttributes;
use mangadex_api_types_rust::{
Language, MangaDexDateTime, MangaSortOrder, OrderDirection, ReferenceExpansionResource,
};
use time::{Duration, OffsetDateTime};
use url::Url;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
// Take the local date and put substract it with 30 days
let created_at_since = OffsetDateTime::now_utc()
.checked_sub(Duration::days(30))
.unwrap();
let created_at_since = MangaDexDateTime::new(&created_at_since);
let res = client
.manga()
.get()
// We pick up all manga that has been created during these last 30 days
.created_at_since(created_at_since)
// Mangadex Popular Titles is ordered by followedCount descending
.order(MangaSortOrder::FollowedCount(OrderDirection::Descending))
// We include the author data
.include(ReferenceExpansionResource::Author)
// We include the arstits data
.include(ReferenceExpansionResource::Artist)
.send()
.await?;
let not_found = String::from("Not found");
// Just a simple index :3
let mut index = 1;
for manga in res.data {
// Find the English title
let title = manga
.attributes
.title
.get(&Language::English)
.unwrap_or(¬_found);
println!("{index} ~ {title}");
// Find the author name
let author = manga
.find_first_relationships(mangadex_api_types::RelationshipType::Author)
.and_then(|e| {
e.attributes.clone().map(|rel| match rel {
RelatedAttributes::Author(a) => a.name,
_ => not_found.clone(),
})
})
.unwrap_or(not_found.clone());
println!("\tAuthor: {author}");
// Find the author name
let artist = manga
.find_first_relationships(mangadex_api_types::RelationshipType::Artist)
.and_then(|e| {
e.attributes.clone().map(|rel| match rel {
RelatedAttributes::Author(a) => a.name,
_ => not_found.clone(),
})
})
.unwrap_or(not_found.clone());
// Print the artist name if it's different of the author
if artist != author {
println!("\tArtist: {artist}");
}
// We generate the link that goes to the Mangadex page
let title_link =
Url::parse("https://mangadex.org/title/")?.join(manga.id.to_string().as_str())?;
println!("\tLink: {title_link}");
println!();
index += 1;
}
println!("Done :3");
Ok(())
}
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.
use reqwest::Client;
use mangadex_api::v5::MangaDexClient;
# async fn run() -> anyhow::Result<()> {
let reqwest_client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let client = MangaDexClient::new(reqwest_client);
# Ok(())
# }
Reference: https://api.mangadex.org/swagger.html#/Manga/get-search-manga
use mangadex_api::v5::MangaDexClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
let manga_results = client
.manga()
.get()
.title("full metal")
.send()
.await?;
println!("manga results = {:?}", manga_results);
Ok(())
}
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:
use mangadex_api::v5::schema::RelatedAttributes;
use mangadex_api::v5::MangaDexClient;
// use mangadex_api_types_rust::{ReferenceExpansionResource, RelationshipType};
use mangadex_api_types::{ReferenceExpansionResource, RelationshipType};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
let manga_results = client
.manga()
.get()
.title("full metal")
.include(&ReferenceExpansionResource::Author)
.send()
.await?;
println!("manga results = {:?}", manga_results);
let authors = manga_results.data.iter().filter_map(|manga| {
manga
.relationships
.iter()
.find(|&rel| rel.type_ == RelationshipType::Author)
});
for author in authors {
if let Some(RelatedAttributes::Author(author_attributes)) = &author.attributes {
println!("{} - {}", author.id, author_attributes.name);
}
}
Ok(())
}
Reference: https://api.mangadex.org/docs/reading-chapter/
// 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 uuid::Uuid;
use mangadex_api::v5::MangaDexClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
// Yeah, i'm a [`100 girlfriend`](https://mangadex.org/title/efb4278c-a761-406b-9d69-19603c5e4c8b/the-100-girlfriends-who-really-really-really-really-really-love-you) simp and what! >:)
let chapter_id = Uuid::parse_str("f2a09509-3c09-4371-a810-ecb99242bd90")?;
let at_home = client
.at_home()
.server()
.id(chapter_id)
.get()
.send()
.await?;
let http_client = reqwest::Client::new();
// Original quality. Use `.data.attributes.data_saver` for smaller, compressed images.
let page_filenames = &at_home.chapter.data;
for filename in page_filenames {
// If using the data-saver option, use "/data-saver/" instead of "/data/" in the URL.
let page_url = at_home
.base_url
.join(&format!(
"/{quality_mode}/{chapter_hash}/{page_filename}",
quality_mode = "data",
chapter_hash = at_home.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(())
}
utils
feature(filename, Result<bytes>)
vector basedNot recommended if you want to handle each response error
use anyhow::Result;
use mangadex_api::{utils::download::chapter::DownloadMode, MangaDexClient};
/// used for file exporting
use std::{
fs::{create_dir_all, File},
io::Write,
};
/// It's from this manga called [`The Grim Reaper Falls In Love With A Human`](https://mangadex.org/title/be2efc56-1669-4e42-9f27-3bd232bca8ea/the-grim-reaper-falls-in-love-with-a-human)
///
/// [Chapter 1 English](https://mangadex.org/chapter/2b4e39a5-fba0-4055-a176-8b7e19faacdb) by [`Kredim`](https://mangadex.org/group/0b870e54-c75f-4d2e-8068-c40f939135fd/kredim)
#[tokio::main]
async fn main() -> Result<()> {
let output_dir = "your-output-dir";
let client = MangaDexClient::default();
let chapter_id = uuid::Uuid::parse_str("32b229f6-e9bf-41a0-9694-63c11191704c")?;
let chapter_files = client
// We use the download builder
.download()
// Chapter id (accept uuid::Uuid)
.chapter(chapter_id)
// You also use `DownloadMode::Normal` if you want some the original quality
//
// Default : Normal
.mode(DownloadMode::DataSaver)
// Enable the [`The MangaDex@Home report`](https://api.mangadex.org/docs/retrieving-chapter/#the-mangadexhome-report-endpoint) 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
.force_port_443(false)
.build()?
.download_element_vec()
.await?;
create_dir_all(format!("{}{}", output_dir, chapter_id))?;
for (filename, bytes_) in chapter_files {
if let Ok(bytes) = bytes_ {
let mut file: File =
File::create(format!("{}{}/{}", output_dir, chapter_id, filename))?;
file.write_all(&bytes)?;
} else if let Err(e) = bytes_ {
eprintln!("{}", e);
}
}
Ok(())
}
tokio-stream
With tokio-stream
, you can handle each response result
use anyhow::Result;
use mangadex_api::{utils::download::chapter::DownloadMode, MangaDexClient};
use std::{
fs::{create_dir_all, 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`](https://mangadex.org/title/1c8f0358-d663-4d60-8590-b5e82890a1e3/keiken-zumi-na-kimi-to-keiken-zero-na-ore-ga-otsukiai-suru-hanashi)
///
/// [Chapter 13 English](https://mangadex.org/chapter/250f091f-4166-4831-9f45-89ff54bf433b) by [`Galaxy Degen Scans`](https://mangadex.org/group/ab24085f-b16c-4029-8c05-38fe16592a85/galaxy-degen-scans)
#[tokio::main]
async fn main() -> Result<()> {
let output_dir = "./test-outputs";
let client = MangaDexClient::default();
let chapter_id = uuid::Uuid::parse_str("250f091f-4166-4831-9f45-89ff54bf433b")?;
create_dir_all(format!("{}/{}", output_dir, chapter_id))?;
let download = client
// We use the download builder
.download()
// Chapter id (accept uuid::Uuid)
.chapter(chapter_id)
// You also use `DownloadMode::Normal` if you want some the original quality
//
// Default : Normal
.mode(DownloadMode::DataSaver)
// Enable the [`The MangaDex@Home report`](https://api.mangadex.org/docs/04-chapter/retrieving-chapter/) if true
//
// Default : false
.report(true)
// Something that i don`t really know about
//
// More details at : https://api.mangadex.org/docs/04-chapter/retrieving-chapter/
.force_port_443(false)
.build()?;
let chapter_files = download.download_stream().await?;
// `pin!` Required for iteration
pin!(chapter_files);
while let Some((data, index, total)) = chapter_files.next().await {
let (filename, bytes_) = data;
// Prin the progression in the standart output
println!("{index} / {total} : {filename} ");
if let Ok(bytes) = bytes_ {
let mut file: File =
File::create(format!("{}/{}/{}", output_dir, chapter_id, filename))?;
file.write_all(&bytes)?;
println!("downloaded");
} else if let Err(e) = bytes_ {
eprintln!("{e}");
}
}
Ok(())
}
The checker is a function called after the response fetching but before retreiving the byte content. Example :
/// 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
use anyhow::Result;
use mangadex_api::{utils::download::chapter::DownloadMode, MangaDexClient};
use std::{
fs::{create_dir_all, 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`](https://mangadex.org/title/1c8f0358-d663-4d60-8590-b5e82890a1e3/keiken-zumi-na-kimi-to-keiken-zero-na-ore-ga-otsukiai-suru-hanashi)
///
/// [Chapter 13 English](https://mangadex.org/chapter/250f091f-4166-4831-9f45-89ff54bf433b) by [`Galaxy Degen Scans`](https://mangadex.org/group/ab24085f-b16c-4029-8c05-38fe16592a85/galaxy-degen-scans)
#[tokio::main]
async fn main() -> Result<()> {
let output_dir = "./test-outputs";
let client = MangaDexClient::default();
let chapter_id = uuid::Uuid::parse_str("250f091f-4166-4831-9f45-89ff54bf433b")?;
create_dir_all(format!("{}/{}", output_dir, chapter_id))?;
let download = client
// We use the download builder
.download()
// Chapter id (accept uuid::Uuid)
.chapter(chapter_id)
// You also use `DownloadMode::Normal` if you want some the original quality
//
// Default : Normal
.mode(DownloadMode::DataSaver)
// Enable the [`The MangaDex@Home report`](https://api.mangadex.org/docs/04-chapter/retrieving-chapter/) if true
//
// Default : false
.report(true)
// Something that i don`t really know about
//
// More details at : https://api.mangadex.org/docs/04-chapter/retrieving-chapter/
.force_port_443(false)
.build()?;
let chapter_files = download
.download_stream_with_checker(move |filename, response| {
let is_skip: bool = {
// Get the response content length
let content_length = match response.content_length() {
None => return false,
Some(d) => d,
};
// open the chapter image file
File::open(format!(
"{}/{}/{}",
output_dir,
chapter_id,
filename.filename.clone()
))
.map(|pre_file| {
pre_file
.metadata()
.map(|metadata| metadata.len() == content_length)
.unwrap_or(false)
})
.unwrap_or(false)
};
is_skip
})
.await?;
// `pin!` Required for iteration
pin!(chapter_files);
while let Some((data, index, total)) = chapter_files.next().await {
let (filename, bytes_) = data;
// Prin the progression in the standart output
println!("{index} / {total} : {filename} ");
if let Ok(bytes) = bytes_ {
let mut file: File =
File::create(format!("{}/{}/{}", output_dir, chapter_id, filename))?;
file.write_all(&bytes)?;
println!("downloaded");
} else if let Err(e) = bytes_ {
eprintln!("{e}");
}
}
Ok(())
}
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.
// 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 mangadex_api::v5::MangaDexClient;
use mangadex_api::CDN_URL;
// use mangadex_api_types_rust::RelationshipType;
use mangadex_api_types::RelationshipType;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = MangaDexClient::default();
let manga_id = Uuid::new_v4();
let manga = client.manga().id(manga_id).get().send().await?;
let cover_id = manga
.data
.relationships
.iter()
.find(|related| related.type_ == RelationshipType::CoverArt)
.expect("no cover art found for manga")
.id;
let cover = client.cover().cover_id(cover_id).get().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 cover_url = Url::parse(&format!(
"{}/covers/{}/{}",
CDN_URL, manga_id, cover.data.attributes.file_name
))
.unwrap();
let http_client = reqwest::Client::new();
let res = http_client.get(cover_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(())
}
utils
feature (recommended) 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(())
}
use anyhow::Result;
use mangadex_api::MangaDexClient;
use std::{fs::File, io::Write};
use uuid::Uuid;
/// 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 output_dir = String::from("test-outputs");
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 bytes = bytes?;
let mut file = File::create(format!("{}/{}", output_dir, filename))?;
file.write_all(&bytes)?;
println!("donwloaded :3");
Ok(())
}
oauth
feature)Before paste copying uhm,... touching the example code below, I recommend that you read the Mangadex Authentification Section
First, register a personal client at Mangadex Profile Settings, and wait until it's approved by staff. It can take 2 or 3 three days for now so just wait :>
After a long time, you can now login
via the oauth
feature.
use mangadex_api::MangaDexClient;
use mangadex_api_schema::v5::oauth::ClientInfo;
use mangadex_api_types::{Password, Username};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut client = MangaDexClient::default();
client
.set_client_info(&ClientInfo {
client_id: String::from("<SET YOUR CLIENT ID HERE>"),
client_secret: String::from("<SET YOUR CLIENT INFO HERE>"),
})
.await?;
let response = client
.oauth()
.login()
.username(Username::parse("<YOUR USERNAME HERE>")?)
.password(Password::parse("<YOUR PASSWORD HERE>")?)
.send()
.await?;
/*
println!("Access Token: {}", response.access_token);
*/
println!("Expires in {} minutes", response.expires_in / 60);
Ok(())
}
You just call mangadex_api::MangaDexClient::oauth().refresh()
...
client
.oauth()
.refresh()
.send()
.await?;
...
Example:
use mangadex_api::MangaDexClient;
// use mangadex_api_schema_rust::v5::oauth::ClientInfo;
// use mangadex_api_types_rust::{Password, Username};
use mangadex_api_schema::v5::oauth::ClientInfo;
use mangadex_api_types::{Password, Username};
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut client = MangaDexClient::default();
// Register your client info
client
.set_client_info(&ClientInfo {
client_id: String::from("<SET YOUR CLIENT ID HERE>"),
client_secret: String::from("<SET YOUR CLIENT INFO HERE>"),
})
.await?;
// Login to your account
let response = client
.oauth()
.login()
.username(Username::parse("<YOUR USERNAME HERE>")?)
.password(Password::parse("<YOUR PASSWORD HERE>")?)
.send()
.await?;
/*
println!("Access Token: {}", response.access_token);
*/
println!("Expires in {} minutes", response.expires_in / 60);
// Wait until the token expires
sleep(Duration::from_secs(<u64 as TryFrom<usize>>::try_from(
response.expires_in,
)?))
.await;
// Refresh the session token
let response = client.oauth().refresh().send().await?;
/*
println!("Access Token: {}", response.access_token);
*/
println!("Expires in {} minutes", response.expires_in / 60);
Ok(())
}
Licensed under either of
at your option.
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.
We welcome contributions from everyone. There are many ways to contribute and the CONTRIBUTING.md document explains how you can contribute and get started.