#下载 #章节 #漫画 #MangaDex #目录 #桌面 #数据

mangadex-desktop-api2

Mangadex漫画、章节和封面下载的Actix服务器

31次发布

1.0.0-alpha.12024年6月19日
0.6.0-alpha.62024年4月25日
0.6.0-alpha.52024年3月11日
0.6.0-alpha.22023年12月29日
0.1.6 2023年3月28日

#95 in 异步

Download history 280/week @ 2024-04-28 16/week @ 2024-05-05 44/week @ 2024-05-19 69/week @ 2024-05-26 22/week @ 2024-06-02 1/week @ 2024-06-09 94/week @ 2024-06-16 450/week @ 2024-06-23 110/week @ 2024-06-30 101/week @ 2024-07-07 239/week @ 2024-07-14 134/week @ 2024-07-21 411/week @ 2024-07-28 108/week @ 2024-08-04 197/week @ 2024-08-11

每月870次下载

MIT/Apache

280KB
7.5K SLoC

mangadex-desktop-api2

Rust

mangadex-desktop-api2 是一个用于从 MangaDex 下载和管理标题、封面和章节的库。

它建立在 mangadex-api 之上。但与SDK不同,它允许您从本地设备下载、读取、更新和删除章节图片、封面和标题元数据。

它也可能获得一个包管理系统,见 #174

如果您正在构建MangaDex桌面应用程序(如 Special Eureka)或私人下载服务,这个库提供了一些可能帮助您的功能

  • 可配置的下载目录:使用 DirsOptions,您可以更改

  • 它是异步的:这个库建立在 actix 演员(不是 actix-web)之上,但它需要您使用 actix 系统处理器作为异步运行时。

    以下是一个使用 actixtauri 的示例

      #[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