#![doc = include_str!("../README.md")]
use dmzj_proto::comic::{ComicChapterResponse, ComicDetailResponse};
use http_cache_reqwest::{
Cache, CacheMode, HttpCache, HttpCacheOptions, MokaManager,
};
use protobuf::Message;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use snafu::ResultExt;
use std::time::Duration;
use tracing::{event, Level};
use crate::crypto::decrypt_bytes;
use crate::error::{DmzjResult, ParseSnafu, ProtoBufSnafu, RequestSnafu};
use crate::model::{
AuthorDetailsResponse, CategoryResponse, LatestUpdatesMangaItem,
MangaSearchResponse, PopularMangaItem,
};
mod crypto;
pub mod error;
pub mod model;
pub const USER_AGENT: &str = "PostmanRuntime/7.28.4";
#[derive(Debug, Clone)]
pub struct Api {
http_client: ClientWithMiddleware,
}
impl Api {
const V3_URL: &'static str = "https://v3api.idmzj.com";
const V3_API_URL: &'static str = "https://nnv3api.idmzj.com";
const V4_API_URL: &'static str = "https://nnv4api.dmzj.com";
pub fn new() -> Self {
let http_client = ClientBuilder::new(
reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(30))
.user_agent(USER_AGENT)
.build()
.expect("failed to build http client"),
)
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: MokaManager::default(),
options: HttpCacheOptions::default(),
}))
.build();
Self { http_client }
}
pub fn from_http_client(http_client: ClientWithMiddleware) -> Self {
Self { http_client }
}
fn popular_manga_url(page: u16) -> String {
format!("{}/classify/0/0/{}.json", Self::V3_URL, page)
}
fn latest_updates_url(page: u16) -> String {
format!("{}/classify/0/1/{}.json", Self::V3_URL, page)
}
fn manga_info_url(id: u32) -> String {
format!("{}/comic/detail/{}?uid=2665531", Self::V4_API_URL, id)
}
fn chapter_images_url(manga_id: u32, chapter_id: i32) -> String {
format!(
"{}/comic/chapter/{}/{}",
Self::V4_API_URL,
manga_id,
chapter_id
)
}
fn category_url() -> String {
format!("{}/0/category.json", Self::V3_API_URL)
}
fn author_details_url(author_tag_id: i64) -> String {
format!("{}/UCenter/author/{}.json", Self::V3_API_URL, author_tag_id)
}
fn search_url<T: AsRef<str>>(keyword: T, page: u16) -> String {
format!(
"{}/search/show/0/{}/{}.json",
Self::V3_API_URL,
keyword.as_ref(),
page
)
}
#[tracing::instrument(skip(self))]
pub async fn fetch_popular_manga(
&self,
page: u16,
) -> DmzjResult<Vec<PopularMangaItem>> {
let url = Self::popular_manga_url(page);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
response.json().await.context(ParseSnafu)
}
#[tracing::instrument(skip(self))]
pub async fn fetch_latest_updates_manga(
&self,
page: u16,
) -> DmzjResult<Vec<LatestUpdatesMangaItem>> {
let url = Self::latest_updates_url(page);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
response.json().await.context(ParseSnafu)
}
#[doc = include_str!("../examples/parse.rs")]
#[tracing::instrument(skip(self))]
pub async fn fetch_manga_details(
&self,
id: u32,
) -> DmzjResult<ComicDetailResponse> {
let url = Self::manga_info_url(id);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
let bytes_from_res = response.bytes().await.context(ParseSnafu)?;
let b = decrypt_bytes(bytes_from_res)?;
ComicDetailResponse::parse_from_bytes(&b).context(ProtoBufSnafu)
}
#[doc = include_str!("../examples/chapter_images.rs")]
#[tracing::instrument(skip(self))]
pub async fn fetch_chapter_images(
&self,
manga_id: u32,
chapter_id: i32,
) -> DmzjResult<ComicChapterResponse> {
let url = Self::chapter_images_url(manga_id, chapter_id);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
let bytes_from_res = response.bytes().await.context(ParseSnafu)?;
let b = decrypt_bytes(bytes_from_res)?;
ComicChapterResponse::parse_from_bytes(&b).context(ProtoBufSnafu)
}
#[doc = include_str!("../examples/category.rs")]
#[tracing::instrument(skip(self))]
pub async fn fetch_category(&self) -> DmzjResult<CategoryResponse> {
let url = Self::category_url();
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
response.json().await.context(ParseSnafu)
}
#[doc = include_str!("../examples/author_details.rs")]
#[tracing::instrument(skip(self))]
pub async fn fetch_author_details(
&self,
author_tag_id: i64,
) -> DmzjResult<AuthorDetailsResponse> {
let url = Self::author_details_url(author_tag_id);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
response.json().await.context(ParseSnafu)
}
#[doc = include_str!("../examples/search.rs")]
#[tracing::instrument(skip(self))]
pub async fn search_manga<T: AsRef<str> + std::fmt::Debug>(
&self,
keyword: T,
page: u16,
) -> DmzjResult<MangaSearchResponse> {
let url = Self::search_url(keyword, page);
event!(Level::DEBUG, url = url.as_str());
let response = self
.http_client
.get(url)
.send()
.await
.context(RequestSnafu)?;
response.json().await.context(ParseSnafu)
}
}
impl Default for Api {
fn default() -> Self {
Self::new()
}
}