31次发布
1.0.0-alpha.1 | 2024年6月19日 |
---|---|
0.6.0-alpha.6 | 2024年4月25日 |
0.6.0-alpha.5 | 2024年3月11日 |
0.6.0-alpha.2 | 2023年12月29日 |
0.1.6 | 2023年3月28日 |
#95 in 异步
每月870次下载
280KB
7.5K SLoC
mangadex-desktop-api2
mangadex-desktop-api2
是一个用于从 MangaDex 下载和管理标题、封面和章节的库。
它建立在 mangadex-api
之上。但与SDK不同,它允许您从本地设备下载、读取、更新和删除章节图片、封面和标题元数据。
它也可能获得一个包管理系统,见 #174
如果您正在构建MangaDex桌面应用程序(如 Special Eureka)或私人下载服务,这个库提供了一些可能帮助您的功能
-
可配置的下载目录:使用
DirsOptions
,您可以更改 -
它是异步的:这个库建立在 actix 演员(不是 actix-web)之上,但它需要您使用 actix 系统处理器作为异步运行时。
#[actix::main] async fn main() { // Since [`actix`] is built on top of [`tokio`], we can use [`tokio::runtime::Handle::current()`] // to share the actix runtime with Tauri tauri::async_runtime::set(tokio::runtime::Handle::current()); // bootstrap the tauri app... // tauri::Builder::default().run().unwrap(); }
功能标志
目前没有!
一些您需要了解的概念
这个库现在使用 Actor模型 来实时跟踪下载过程。
每个下载任务:章节/封面图片下载和标题元数据都是一个演员,我称之为 Task
。您可以监听任务以了解其状态,例如是否挂起、正在加载、成功或错误。每个任务都有一个唯一的ID,对应于其下载的内容。下载漫画将产生一个 MangaDownloadTask
演员实例,封面将产生一个 CoverDownloadTask
,等等...
任务由对应任务类型的经理管理,这意味着存在一个ChapterDownloadManager
、一个CoverDownloadManager
以及一个MangaDownloadManager
。
为了管理这一切,存在一个顶层DownloadManger
,它允许您与经理交互。但它也包含一个内部状态,允许与底层的MangaDex API 客户端、DirOptions
API以及下载历史记录演员交互。
DirOptions
API
DirOptions
API管理与文件系统的所有交互。您可以获取、创建/更新数据以及删除数据,如元数据和图像。
获取数据
要获取数据,可以使用DataPulls
。
use actix::prelude::*;
use mangadex_api_types_rust::{MangaSortOrder, OrderDirection};
/// Yes! we have a prelude module too.
use mangadex_desktop_api2::prelude::*;
fn main() -> anyhow::Result<()> {
/// start a actix system
let run = System::new();
/// Runs your async code with `.block_on`
run.block_on(async {
/// Init the dir option api
let options = DirsOptions::new_from_data_dir("data");
/// Verify and init the required directories.
/// This is mostly not required because `.start()` automatically call `.verify_and_init()`
options.verify_and_init()?;
/// init the actor
let options_actor = options.start();
/// init a data pull
let data_pull = options_actor
.get_manga_list()
.await?
/// Yes, you can sort data now
.to_sorted(MangaSortOrder::Year(OrderDirection::Ascending))
.await;
/// Iterate over the results
for manga in data_pull {
println!("{:#?} - {has_failed}", manga.id);
if let Some(year) = manga.attributes.year {
println!("year {year}",)
}
}
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
创建/更新数据(即推送)
要推送数据,可以使用Push
特质。
use std::collections::HashMap;
/// This example will illustrate how to push data to a
/// You need to enable the `macros` feature for `actix` to make this example work.
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<()> {
// Init the dir options api
let options = DirsOptions::new_from_data_dir("output").start();
// Cover, author and artists is required as relationship
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 {
// Totally an idea that i found myself :D
title: HashMap::from([(Language::English, String::from("Dating a V-Tuber"))]),
// Sorry, i use google traduction for this one.
alt_titles: vec![HashMap::from([(Language::Japanese, String::from("VTuberとの出会い"))])],
available_translated_languages: vec![Language::English, Language::French],
// Hahaha... I wish it will got serialized very soon xD
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,
// You can put any tag that you want
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]
};
// Just call `.push()`
options.push(my_manga).await?;
Ok(())
}
删除数据
要删除数据,可以使用Delete
特质。
use std::str::FromStr;
use actix::prelude::*;
use mangadex_desktop_api2::prelude::*;
use tokio_stream::StreamExt;
use uuid::Uuid;
fn main() -> anyhow::Result<()> {
// Init the actix system runner
let run = System::new();
run.block_on(async {
// Start the option actor
let options_actor = DirsOptions::new_from_data_dir("data").start();
let manga_id = Uuid::from_str("b4c93297-b32f-4f90-b619-55456a38b0aa")?;
// You can just call `.delete_manga(Uuid)` to delete a give manga
let data = options_actor.delete_manga(manga_id).await?;
// The `MangaDeleteData` consists of `covers` field which is the deleted covers ids
// and `chapters` field which is the deleted chapters ids
println!("{:#?}", data);
// Get all the manga chapter
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
};
// check if there is no chapters left
assert!(chapters.is_empty(), "Some chapter still remains");
// check if there is no covers left
assert!(covers.is_empty(), "Some covers still remains");
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
DownloadHistory
API
DownloadHistory
API的唯一目的是跟踪下载错误。这意味着如果您有未完成的下载或失败的下载,您应该能够看到它。您可以通过HistoryActorService
与之交互,但在插入或删除条目时要小心。这可能会破坏您的应用程序。
许可
从v1版本开始,此软件包现在有MIT许可,所以这是您的选择 :)。
依赖关系
~14–27MB
~420K SLoC