mangadex-desktop-api2

mangadex-desktop-api2 is a library for downloading and managing titles, covers, and chapters from MangaDex.
This is built on top of the mangadex-api.
But unlike the SDK, it allows you to download, read, update and delete chapter images, covers and titles metadata from your local device.
It might also get a package management system, sees #174
If you're building a MangaDex desktop app (like Special Eureka) or a private downloading service,
this library provide a bunch of features that might help you:
-
Configurable download directory: With the DirsOptions, you can change the
-
It's asynchronous: This library is built on top of actix actors (not actix-web)
but it requires you to use actix system handler as an asynchronous runtime.
Here is an example of using actix with tauri:
#[actix::main]
async fn main() {
tauri::async_runtime::set(tokio::runtime::Handle::current());
}
Feature flags
None for now!
Some concepts that you need to know
This library now uses Actor model to track the download process in real-time.
Each downloading task: chapter / cover images downloading and title metadata is an actor and which I call Task.
You can listen to a task to know it's state like if it's pending, loading, success, or error.
Each task have an unique id, corresponding to what it's downloading.
Downloading a manga will spawn a MangaDownloadTask actor, cover will spawn a CoverDownloadTask, etc...
A Task is managed by a manager corresponding by the task type,
which means that there is a ChapterDownloadManager, a CoverDownloadManager and a MangaDownloadManager.
To manage all of this there is a top level DownloadManger that allows you to interact with the manager.
But it also contain an inner state allows to interact with the underlying MangaDex API Client, DirOptions API and the download history actor.
DirOptions API
The DirOptions API manages every interaction to the filesystem.
You can get, create/update data and delete data like metadatas and images.
Getting data
To get data, it is done with the DataPulls.
use actix::prelude::*;
use mangadex_api_types_rust::{MangaSortOrder, OrderDirection};
use mangadex_desktop_api2::prelude::*;
fn main() -> anyhow::Result<()> {
let run = System::new();
run.block_on(async {
let options = DirsOptions::new_from_data_dir("data");
options.verify_and_init()?;
let options_actor = options.start();
let data_pull = options_actor
.get_manga_list()
.await?
.to_sorted(MangaSortOrder::Year(OrderDirection::Ascending))
.await;
for manga in data_pull {
println!("{:#?} - {has_failed}", manga.id);
if let Some(year) = manga.attributes.year {
println!("year {year}",)
}
}
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
Create/update data (aka push)
To push data, it is done with the Push trait.
use std::collections::HashMap;
use actix::prelude::*;
use mangadex_api_schema_rust::{
v5::{
AuthorAttributes, CoverAttributes, MangaAttributes, RelatedAttributes, Relationship,
TagAttributes,
},
ApiObject,
};
use mangadex_api_types_rust::{
ContentRating, Demographic, Language, MangaState, MangaStatus, RelationshipType, Tag,
};
use mangadex_desktop_api2::prelude::*;
use url::Url;
use uuid::Uuid;
#[actix::main]
async fn main() -> anyhow::Result<()> {
let options = DirsOptions::new_from_data_dir("output").start();
let author = Relationship {
id: Uuid::new_v4(),
type_: RelationshipType::Author,
related: None,
attributes: Some(RelatedAttributes::Author(AuthorAttributes {
name: String::from("Tony Mushah"),
image_url: Some(String::from(
"https://avatars.githubusercontent.com/u/95529016?v=4",
)),
biography: Default::default(),
twitter: Url::parse("https://twitter.com/tony_mushah").ok(),
pixiv: None,
melon_book: None,
fan_box: None,
booth: None,
nico_video: None,
skeb: None,
fantia: None,
tumblr: None,
youtube: None,
weibo: None,
naver: None,
namicomi: None,
website: Url::parse("https://github.com/tonymushah").ok(),
version: 1,
created_at: Default::default(),
updated_at: Default::default(),
})),
};
let artist = {
let mut author_clone = author.clone();
author_clone.type_ = RelationshipType::Artist;
author_clone
};
let cover = Relationship {
id: Uuid::new_v4(),
type_: RelationshipType::CoverArt,
related: None,
attributes: Some(RelatedAttributes::CoverArt(CoverAttributes {
description: String::default(),
locale: Some(Language::Japanese),
volume: Some(String::from("1")),
file_name: String::from("somecover.png"),
created_at: Default::default(),
updated_at: Default::default(),
version: 1,
})),
};
let my_manga = ApiObject {
id: Uuid::new_v4(),
type_: RelationshipType::Manga,
attributes: MangaAttributes {
title: HashMap::from([(Language::English, String::from("Dating a V-Tuber"))]),
alt_titles: vec![HashMap::from([(Language::Japanese, String::from("VTuberとの出会い"))])],
available_translated_languages: vec![Language::English, Language::French],
description: HashMap::from([(Language::English, String::from("For some reason, me #Some Guy# is dating \"Sakachi\", the biggest V-Tuber all over Japan. But we need to keep it a secret to not terminate her V-Tuber career. Follow your lovey-dovey story, it might be worth it to read it."))]),
is_locked: false,
links: None,
original_language: Language::Malagasy,
last_chapter: None,
last_volume: None,
publication_demographic: Some(Demographic::Shounen),
state: MangaState::Published,
status: MangaStatus::Ongoing,
year: Some(2025),
content_rating: Some(ContentRating::Suggestive),
chapter_numbers_reset_on_new_volume: false,
latest_uploaded_chapter: None,
tags: vec![ApiObject {
id: Tag::Romance.into(),
type_: RelationshipType::Tag,
attributes: TagAttributes {
name: HashMap::from([(Language::English, Tag::Romance.to_string())]),
description: Default::default(),
group: Tag::Romance.into(),
version: 1
},
relationships: Default::default()
}, ApiObject {
id: Tag::AwardWinning.into(),
type_: RelationshipType::Tag,
attributes: TagAttributes {
name: HashMap::from([(Language::English, Tag::AwardWinning.to_string())]),
description: Default::default(),
group: Tag::AwardWinning.into(),
version: 1
},
relationships: Default::default()
}, ApiObject {
id: Tag::Drama.into(),
type_: RelationshipType::Tag,
attributes: TagAttributes {
name: HashMap::from([(Language::English, Tag::Drama.to_string())]),
description: Default::default(),
group: Tag::Drama.into(),
version: 1
},
relationships: Default::default()
}, ApiObject {
id: Tag::SliceOfLife.into(),
type_: RelationshipType::Tag,
attributes: TagAttributes {
name: HashMap::from([(Language::English, Tag::SliceOfLife.to_string())]),
description: Default::default(),
group: Tag::SchoolLife.into(),
version: 1
},
relationships: Default::default()
}],
created_at: Default::default(),
updated_at: Default::default(),
version: 1
},
relationships: vec![author, artist, cover]
};
options.push(my_manga).await?;
Ok(())
}
Deleting data
To delete data, it is done with the Delete traits.
use std::str::FromStr;
use actix::prelude::*;
use mangadex_desktop_api2::prelude::*;
use tokio_stream::StreamExt;
use uuid::Uuid;
fn main() -> anyhow::Result<()> {
let run = System::new();
run.block_on(async {
let options_actor = DirsOptions::new_from_data_dir("data").start();
let manga_id = Uuid::from_str("b4c93297-b32f-4f90-b619-55456a38b0aa")?;
let data = options_actor.delete_manga(manga_id).await?;
println!("{:#?}", data);
let chapters: Vec<Uuid> = {
let params = ChapterListDataPullFilterParams {
manga_id: Some(manga_id),
..Default::default()
};
options_actor
.get_chapters()
.await?
.to_filtered(params)
.map(|o| o.id)
.collect()
.await
};
let covers: Vec<Uuid> = {
let params = CoverListDataPullFilterParams {
manga_ids: [manga_id].into(),
..Default::default()
};
options_actor
.get_covers()
.await?
.to_filtered(params)
.map(|o| o.id)
.collect()
.await
};
assert!(chapters.is_empty(), "Some chapter still remains");
assert!(covers.is_empty(), "Some covers still remains");
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
The DownloadHistory API
Only purpose of the DownloadHistory API is to track download errors.
Which means that if you have an unfinished download or an failed download, you should be able to see it.
You can interact with it with the HistoryActorService, but be careful when inserting or removing entries.
It could pontentialy break your app.
Licence
Since v1, this package has now an MIT licence,
so it's your choice :).