Initial commit

This commit is contained in:
blank X 2021-02-16 19:38:06 +07:00
commit 27c8c76dfe
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
11 changed files with 1839 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1085
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "mangadexrs"
version = "0.1.0"
authors = ["blank X <theblankx@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.release]
lto = true
[dependencies]
tokio = { version = "1.1", features = ["rt-multi-thread"] }
reqwest = "0.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "2.33", default-features = false }
quick-xml = "0.20"
regex = "1.4"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 blank X
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

194
src/commands/download.rs Normal file
View File

@ -0,0 +1,194 @@
use crate::utils;
use crate::structs;
use std::sync::Arc;
use std::fs::create_dir_all;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::process::exit;
use clap::ArgMatches;
use tokio::sync::{Mutex, RwLock};
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
extern crate tokio;
extern crate reqwest;
const DOWNLOAD_WORKERS: usize = 5;
const NON_IMAGE_WAIT_TIME: Duration = Duration::from_millis(5000);
const NO_ITEM_WAIT_TIME: Duration = Duration::from_millis(1000);
const GET_DATA_FAIL_WAIT_TIME: Duration = Duration::from_millis(30000);
pub async fn download(arg_m: &ArgMatches<'_>) {
let print_only = arg_m.is_present("print");
let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect();
let mut chapter_ids: Vec<_> = arg_m.values_of("chapter_ids").unwrap_or_default().map(|i| i.parse::<i32>().unwrap()).collect();
chapter_ids.sort();
chapter_ids.dedup();
let mut manga_ids: Vec<_> = arg_m.values_of("manga_ids").unwrap_or_default().map(|i| i.parse::<i32>().unwrap()).collect();
manga_ids.sort();
manga_ids.dedup();
let client = reqwest::Client::new();
let mut return_fail = false;
let mutex = Arc::new(Mutex::new(DownloadData { data: VecDeque::new(), is_done: false }));
let handles = summon_handles(client.clone(), Arc::clone(&mutex)).await;
handle_chapters(client.clone(), chapter_ids.clone(), Arc::clone(&mutex), &mut return_fail, print_only).await;
for manga_id in manga_ids {
let cloned_client = client.clone();
let cloned_mutex = Arc::clone(&mutex);
loop {
let mut manga = match utils::get_manga(cloned_client.clone(), manga_id).await {
Ok(Some(i)) => i,
Ok(None) => {
eprintln!("Manga ID: {}\nError: does not exist", manga_id);
return_fail = true;
break;
},
Err(err) => {
eprintln!("Manga ID: {}\nError: {}", manga_id, err);
sleep(GET_DATA_FAIL_WAIT_TIME).await;
continue;
}
};
let mut mchapter_ids = Vec::new();
manga.data.chapters.reverse();
for chapter in manga.data.chapters {
if chapter_ids.contains(&chapter.id) || (!languages.is_empty() && !languages.contains(&chapter.language.as_str())) {
continue;
}
mchapter_ids.push(chapter.id);
}
if !mchapter_ids.is_empty() {
handle_chapters(cloned_client.clone(), mchapter_ids, Arc::clone(&cloned_mutex), &mut return_fail, print_only).await;
}
break;
}
}
mutex.lock().await.is_done = true;
for handle in handles {
handle.await.unwrap();
}
if return_fail {
exit(1);
}
}
async fn handle_chapters(client: reqwest::Client, chapter_ids: Vec<i32>, mutex: Arc<Mutex<DownloadData>>, return_fail: &mut bool, print_only: bool) {
let mut chapter_datas = Vec::new();
for chapter_id in chapter_ids {
let cloned_client = client.clone();
loop {
let chapter = match utils::get_chapter(cloned_client.clone(), chapter_id).await {
Ok(Some(i)) => i,
Ok(None) => {
eprintln!("Chapter ID: {}\nError: does not exist", chapter_id);
*return_fail = true;
break;
},
Err(err) => {
eprintln!("Chapter ID: {}\nError: {}", chapter_id, err);
sleep(GET_DATA_FAIL_WAIT_TIME).await;
continue;
}
};
if print_only {
println!("{}", chapter.data.pages.iter().map(|i| format!("{}{}/{}", &chapter.data.server, &chapter.data.hash, i)).collect::<Vec<_>>().join(";"));
} else {
let manga_slug = utils::generate_slug(&chapter.data.manga_title);
let local_dir: PathBuf = [&manga_slug, &chapter.data.chapter].iter().collect();
let chapter_pages = chapter.data.pages.clone();
let chapter_rwlock = Arc::new(RwLock::new((chapter.data, 0)));
for (i, server_file) in chapter_pages.iter().enumerate() {
let mut local_file = local_dir.clone();
local_file.push((i + 1).to_string());
if let Some(ext) = Path::new(&server_file).extension() {
local_file.set_extension(ext);
}
if local_file.exists() {
continue;
}
chapter_datas.push((Arc::clone(&chapter_rwlock), i, local_file));
}
}
break;
}
}
if !chapter_datas.is_empty() {
mutex.lock().await.data.extend(chapter_datas);
}
}
async fn summon_handles(client: reqwest::Client, mutex: Arc<Mutex<DownloadData>>) -> Vec<JoinHandle<()>> {
let mut handles = Vec::with_capacity(DOWNLOAD_WORKERS);
for worker_id in 0..DOWNLOAD_WORKERS {
let tcloned_mutex = Arc::clone(&mutex);
let tcloned_client = client.clone();
handles.push(tokio::spawn(async move {
eprintln!("[DW{}] Up!", worker_id);
loop {
let cloned_mutex = Arc::clone(&tcloned_mutex);
let cloned_client = tcloned_client.clone();
let mut download_data = cloned_mutex.lock().await;
let data = download_data.data.pop_front();
let is_done = download_data.is_done;
drop(download_data);
let (chapter_rwlock, page_index, local_file) = match data {
Some(data) => data,
None => {
if is_done {
break;
}
sleep(NO_ITEM_WAIT_TIME).await;
continue;
}
};
let read_guard = chapter_rwlock.read().await;
let chapter_id = read_guard.0.id;
let mut server = read_guard.0.server.clone();
let mut server_fallback = read_guard.0.server_fallback.clone();
let mut hash = read_guard.0.hash.clone();
let mut server_file = read_guard.0.pages[page_index].clone();
let mut local_update_count = read_guard.1;
drop(read_guard);
if let Some(parent) = local_file.parent() {
match create_dir_all(&parent) {
Ok(()) => (),
Err(err) => eprintln!("[DW{}] Error while creating directories {}: {}", worker_id, parent.display(), err)
};
}
eprintln!("[DW{}] Downloading {}", worker_id, local_file.display());
loop {
match utils::download_page(cloned_client.clone(), &server, &server_fallback, &hash, &server_file, &local_file).await {
Ok(true) => break,
Ok(false) => eprintln!("[DW{}] Error while downloading {}: does not exist", worker_id, local_file.display()),
Err(err) => eprintln!("[DW{}] Error while downloading {}: {}", worker_id, local_file.display(), err)
};
sleep(NON_IMAGE_WAIT_TIME).await;
let mut write_guard = chapter_rwlock.write().await;
if local_update_count >= write_guard.1 {
match utils::get_chapter(cloned_client.clone(), chapter_id).await {
Ok(Some(chapter_data)) => {
write_guard.0 = chapter_data.data;
write_guard.1 = local_update_count + 1;
},
Ok(None) => eprintln!("[DW{}] Error while fetching chapter data {}: does not exist", worker_id, chapter_id),
Err(err) => eprintln!("[DW{}] Error while fetching chapter data {}: {}", worker_id, chapter_id, err)
};
}
server = write_guard.0.server.clone();
server_fallback = write_guard.0.server_fallback.clone();
hash = write_guard.0.hash.clone();
server_file = write_guard.0.pages[page_index].clone();
local_update_count = write_guard.1;
}
eprintln!("[DW{}] Downloaded {}", worker_id, local_file.display());
}
eprintln!("[DW{}] Down!", worker_id);
}));
}
handles
}
struct DownloadData {
pub data: VecDeque<(Arc<RwLock<(structs::ChapterData, i32)>>, usize, PathBuf)>,
pub is_done: bool
}

104
src/commands/feed.rs Normal file
View File

@ -0,0 +1,104 @@
use crate::utils;
use std::io::Cursor;
use std::process::exit;
use clap::ArgMatches;
use quick_xml::Writer;
use quick_xml::events::{Event, BytesStart, BytesText, BytesEnd};
extern crate reqwest;
pub async fn feed(arg_m: &ArgMatches<'_>) {
let client = reqwest::Client::new();
let manga_id = arg_m.value_of("id").unwrap();
let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect();
let manga_info = match utils::get_manga(client, manga_id.parse().unwrap()).await {
Ok(Some(manga_info)) => manga_info,
Ok(None) => {
eprintln!("ID: {}\nError: does not exist", &manga_id);
exit(1);
},
Err(err) => {
eprintln!("ID: {}\nError: {}", &manga_id, err);
exit(1);
}
};
let mut writer = Writer::new(Cursor::new(Vec::new()));
{
let mut elem = BytesStart::owned(b"rss".to_vec(), 3);
elem.push_attribute(("version", "2.0"));
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesStart::owned(b"channel".to_vec(), 7);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesStart::owned(b"title".to_vec(), 5);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&manga_info.data.manga.title).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"title".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"link".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&format!("https://mangadex.org/title/{}", &manga_id)).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"link".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesStart::owned(b"description".to_vec(), 11);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&manga_info.data.manga.description).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"description".to_vec());
writer.write_event(Event::End(elem)).unwrap();
}
for chapter in manga_info.data.chapters {
if !languages.is_empty() && !languages.contains(&chapter.language.as_str()) {
continue;
}
let link = format!("https://mangadex.org/chapter/{}", &chapter.id);
let elem = BytesStart::owned(b"item".to_vec(), 4);
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesStart::owned(b"title".to_vec(), 5);
writer.write_event(Event::Start(elem)).unwrap();
let mut title = chapter.to_string();
if Some(chapter.chapter.clone()) == manga_info.data.manga.last_chapter {
title.push_str(" [END]");
}
let elem = BytesText::from_plain_str(&title).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"title".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let mut elem = BytesStart::owned(b"guid".to_vec(), 4);
elem.push_attribute(("isPermaLink", "true"));
writer.write_event(Event::Start(elem)).unwrap();
let elem = BytesText::from_plain_str(&link).into_owned();
writer.write_event(Event::Text(elem)).unwrap();
let elem = BytesEnd::owned(b"guid".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesEnd::owned(b"item".to_vec());
writer.write_event(Event::End(elem)).unwrap();
}
let elem = BytesEnd::owned(b"channel".to_vec());
writer.write_event(Event::End(elem)).unwrap();
let elem = BytesEnd::owned(b"rss".to_vec());
writer.write_event(Event::End(elem)).unwrap();
println!("{}", String::from_utf8(writer.into_inner().into_inner()).unwrap());
}

6
src/commands/mod.rs Normal file
View File

@ -0,0 +1,6 @@
mod view;
mod feed;
mod download;
pub use view::view;
pub use feed::feed;
pub use download::download;

78
src/commands/view.rs Normal file
View File

@ -0,0 +1,78 @@
use crate::utils;
use std::process::exit;
use clap::ArgMatches;
extern crate tokio;
extern crate reqwest;
pub async fn view(arg_m: &ArgMatches<'_>) {
let client = reqwest::Client::new();
let languages: Vec<_> = arg_m.values_of("language").unwrap_or_default().collect();
let handles = arg_m.values_of("id").unwrap().map(|id| {
let cloned_client = client.clone();
let id = id.to_string();
let cid = id.parse().unwrap();
(tokio::spawn(async move {
utils::get_manga(cloned_client, cid).await
}), id)
}).collect::<Vec<_>>();
let mut fail = false;
let mut one_done = false;
for handle in handles {
let (handle, id) = handle;
let manga = match handle.await {
Ok(manga) => manga,
Err(err) => {
if one_done {
eprintln!("");
}
eprintln!("ID: {}\nError: {}", id, err);
fail = true;
one_done = true;
continue;
}
};
match manga {
Ok(manga) => {
match manga {
Some(mut manga) => {
let mut text = manga.data.manga.to_string();
if one_done {
text = format!("\n{}", text);
}
text.push_str("\nChapters:");
manga.data.chapters.reverse();
for chapter in &manga.data.chapters {
if !languages.is_empty() && !languages.contains(&chapter.language.as_str()) {
continue;
}
text.push_str(&format!("\n- {}", chapter));
if Some(chapter.chapter.clone()) == manga.data.manga.last_chapter {
text.push_str(" [END]");
}
}
println!("{}", text);
},
None => {
if one_done {
eprintln!("");
}
eprintln!("ID: {}\nError: does not exist", id);
fail = true;
}
};
},
Err(err) => {
if one_done {
eprintln!("");
}
eprintln!("ID: {}\nError: {}", id, err);
fail = true;
}
};
one_done = true;
}
if fail {
exit(1);
}
}

102
src/main.rs Normal file
View File

@ -0,0 +1,102 @@
mod commands;
mod structs;
mod utils;
use clap::{App, AppSettings, Arg, SubCommand};
extern crate tokio;
const LANGUAGES: &[&str] = &["sa", "bd", "bg", "mm", "ct", "cn", "hk", "cz", "dk", "nl", "gb", "ph", "fi", "fr", "de", "gr", "il", "in", "hu", "id", "it", "jp", "kr", "lt", "my", "mn", "no", "other", "ir", "pl", "br", "pt", "ro", "ru", "rs", "es", "mx", "se", "th", "tr", "ua", "vn"];
fn validate_int(i: String) -> Result<(), String> {
match i.parse::<i32>() {
Ok(_) => Ok(()),
Err(err) => Err(err.to_string())
}
}
fn main() {
let matches = App::new("mangadexrs")
.about("mangadex.org downloader in rust")
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("view")
.aliases(&["info", "show"])
.arg(
Arg::with_name("language")
.long("language")
.short("l")
.takes_value(true)
.multiple(true)
.possible_values(LANGUAGES)
.help("Only show chapters of the specified languages")
).arg(
Arg::with_name("id")
.takes_value(true)
.multiple(true)
.required(true)
.validator(validate_int)
)
)
.subcommand(
SubCommand::with_name("feed")
.alias("rss")
.arg(
Arg::with_name("language")
.long("language")
.short("l")
.takes_value(true)
.multiple(true)
.possible_values(LANGUAGES)
.help("Only show chapters of the specified languages")
).arg(
Arg::with_name("id")
.takes_value(true)
.required(true)
.validator(validate_int)
)
)
.subcommand(
SubCommand::with_name("download")
.alias("dl")
.arg(
Arg::with_name("print")
.long("print")
.short("p")
.help("Print the URL to download only")
).arg(
Arg::with_name("manga_ids")
.long("manga-ids")
.short("m")
.takes_value(true)
.multiple(true)
.required_unless("chapter_ids")
.help("Add chapters from the manga specified, limit languages with --language")
).arg(
Arg::with_name("language")
.long("language")
.short("l")
.takes_value(true)
.multiple(true)
.possible_values(LANGUAGES)
.help("Filter languages for manga_ids, has no effect on chapter_ids")
).arg(
Arg::with_name("chapter_ids")
.takes_value(true)
.required_unless("manga_ids")
.multiple(true)
.validator(validate_int)
)
)
.get_matches();
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
match matches.subcommand() {
("view", Some(sub_m)) => runtime.block_on(commands::view(sub_m)),
("feed", Some(sub_m)) => runtime.block_on(commands::feed(sub_m)),
("download", Some(sub_m)) => runtime.block_on(commands::download(sub_m)),
_ => panic!("AppSettings::SubcommandRequiredElseHelp do your job please")
};
}

173
src/structs.rs Normal file
View File

@ -0,0 +1,173 @@
use std::io;
use std::fmt;
use std::marker::PhantomData;
use std::collections::BTreeMap;
use regex::Regex;
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer};
extern crate reqwest;
extern crate serde_json;
const TAGS_AND_TYPE: &[(&str, &str)] = &[("4-Koma", "Format"), ("Action", "Genre"), ("Adventure", "Genre"), ("Award Winning", "Format"), ("Comedy", "Genre"), ("Cooking", "Theme"), ("Doujinshi", "Format"), ("Drama", "Genre"), ("Ecchi", "Content"), ("Fantasy", "Genre"), ("Gyaru", "Theme"), ("Harem", "Theme"), ("Historical", "Genre"), ("Horror", "Genre"), ("Martial Arts", "Theme"), ("Mecha", "Genre"), ("Medical", "Genre"), ("Music", "Theme"), ("Mystery", "Genre"), ("Oneshot", "Format"), ("Psychological", "Genre"), ("Romance", "Genre"), ("School Life", "Theme"), ("Sci-Fi", "Genre"), ("Shoujo Ai", "Genre"), ("Shounen Ai", "Genre"), ("Slice of Life", "Genre"), ("Smut", "Content"), ("Sports", "Genre"), ("Supernatural", "Theme"), ("Tragedy", "Genre"), ("Long Strip", "Format"), ("Yaoi", "Genre"), ("Yuri", "Genre"), ("Video Games", "Theme"), ("Isekai", "Genre"), ("Adaptation", "Format"), ("Anthology", "Format"), ("Web Comic", "Format"), ("Full Color", "Format"), ("User Created", "Format"), ("Official Colored", "Format"), ("Fan Colored", "Format"), ("Gore", "Content"), ("Sexual Violence", "Content"), ("Crime", "Genre"), ("Magical Girls", "Genre"), ("Philosophical", "Genre"), ("Superhero", "Genre"), ("Thriller", "Genre"), ("Wuxia", "Genre"), ("Aliens", "Theme"), ("Animals", "Theme"), ("Crossdressing", "Theme"), ("Demons", "Theme"), ("Delinquents", "Theme"), ("Genderswap", "Theme"), ("Ghosts", "Theme"), ("Monster Girls", "Theme"), ("Loli", "Theme"), ("Magic", "Theme"), ("Military", "Theme"), ("Monsters", "Theme"), ("Ninja", "Theme"), ("Office Workers", "Theme"), ("Police", "Theme"), ("Post-Apocalyptic", "Theme"), ("Reincarnation", "Theme"), ("Reverse Harem", "Theme"), ("Samurai", "Theme"), ("Shota", "Theme"), ("Survival", "Theme"), ("Time Travel", "Theme"), ("Vampires", "Theme"), ("Traditional Games", "Theme"), ("Virtual Reality", "Theme"), ("Zombies", "Theme"), ("Incest", "Theme"), ("Mafia", "Theme"), ("Villainess", "Theme")];
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MangaInfo {
pub id: i32,
pub title: String,
#[serde(deserialize_with = "sanitize_description")]
pub description: String,
pub artist: Vec<String>,
pub author: Vec<String>,
pub tags: Vec<usize>,
pub views: usize,
pub last_chapter: Option<String>
}
#[derive(Deserialize, Debug)]
pub struct MangaData {
pub manga: MangaInfo,
pub chapters: Vec<MiniChapterData>
}
#[derive(Deserialize, Debug)]
pub struct Manga {
pub data: MangaData
}
#[derive(Deserialize, Debug)]
pub struct MiniChapterData {
pub id: i32,
pub chapter: String,
#[serde(default)]
pub volume: String,
pub title: String,
pub language: String
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChapterData {
pub id: i32,
pub manga_title: String,
pub chapter: String,
pub hash: String,
pub pages: Vec<String>,
pub server: String,
#[serde(default)]
pub server_fallback: String
}
#[derive(Deserialize, Debug)]
pub struct Chapter {
pub data: ChapterData
}
impl fmt::Display for MangaInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut text = format!("ID: {}\nTitle: {}\nViews: {}\n", self.id, &self.title, self.views);
if !self.artist.is_empty() {
text.push_str(&format!("Artists: {}\n", &self.artist.join(", ")));
}
if !self.author.is_empty() {
text.push_str(&format!("Authors: {}\n", &self.author.join(", ")));
}
if !self.tags.is_empty() {
let mut treemap = BTreeMap::new();
for tag_id in &self.tags {
let (tag_name, tag_group) = match TAGS_AND_TYPE.get(tag_id - 1) {
Some(&(i, j)) => (i.to_string(), j),
None => (tag_id.to_string(), "Unknown")
};
treemap.entry(tag_group.clone()).or_insert_with(|| Vec::new()).push(tag_name);
}
for (tag_key, tag_value) in treemap {
text.push_str(&format!("{}: {}\n", tag_key, tag_value.join(", ")));
}
}
write!(formatter, "{}Description:\n{}", &text, &self.description)
}
}
impl fmt::Display for MiniChapterData {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut text = format!("{}: ", self.id);
if !self.volume.is_empty() {
text.push_str(&format!("Volume {}: ", &self.volume));
}
text.push_str(&format!("Chapter {}", &self.chapter));
if !self.title.is_empty() {
text.push_str(&format!(": {}", &self.title));
}
write!(formatter, "{} [{}]", &text, &self.language)
}
}
fn sanitize_description<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>
{
struct SanitizeDescription<T>(PhantomData<fn() -> T>);
impl<'de> Visitor<'de> for SanitizeDescription<String>
{
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("unknown error with sanitize_description idfk lmao")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error
{
Ok(match Regex::new(r"\[\w+\].+\[/\w+\]") {
Ok(regex) => regex.replace_all(&value, "").trim().to_string(),
Err(_) => value.to_string()
})
}
}
deserializer.deserialize_any(SanitizeDescription(PhantomData))
}
#[derive(Debug)]
pub enum Error {
IO(io::Error),
Reqwest(reqwest::Error),
SerdeJSON(serde_json::Error),
}
impl From<io::Error> for Error {
#[inline]
fn from(error: io::Error) -> Error {
Error::IO(error)
}
}
impl From<reqwest::Error> for Error {
#[inline]
fn from(error: reqwest::Error) -> Error {
Error::Reqwest(error)
}
}
impl From<serde_json::Error> for Error {
#[inline]
fn from(error: serde_json::Error) -> Error {
Error::SerdeJSON(error)
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
&match self {
Error::IO(err) => format!("io error: {}", err),
Error::Reqwest(err) => format!("reqwest error: {}", err),
Error::SerdeJSON(err) => format!("serde_json error: {}", err),
}
)
}
}

56
src/utils.rs Normal file
View File

@ -0,0 +1,56 @@
use crate::structs;
use std::fs::write;
use std::path::Path;
extern crate reqwest;
extern crate serde_json;
pub fn generate_slug(text: &str) -> String {
text.to_lowercase().trim().replace(|c: char| !c.is_alphanumeric(), "-")
}
pub async fn get_manga(client: reqwest::Client, id: i32) -> Result<Option<structs::Manga>, structs::Error> {
let resp = client.get(&format!("https://mangadex.org/api/v2/manga/{}?include=chapters", &id))
.send()
.await?;
if resp.status() == 404 {
return Ok(None);
}
resp.error_for_status_ref()?;
Ok(Some(serde_json::from_str(&resp.text().await?)?))
}
pub async fn get_chapter(client: reqwest::Client, chapter: i32) -> Result<Option<structs::Chapter>, structs::Error> {
let resp = client.get(&format!("https://mangadex.org/api/v2/chapter/{}", &chapter))
.send()
.await?;
if resp.status() == 404 {
return Ok(None);
}
resp.error_for_status_ref()?;
Ok(Some(serde_json::from_str(&resp.text().await?)?))
}
pub async fn download_page(client: reqwest::Client, server: &str, server_fallback: &str, hash: &str, server_file: &str, local_file: &Path) -> Result<bool, structs::Error> {
let res = download(client.clone(), &format!("{}{}/{}", &server, &hash, &server_file), &local_file).await;
if server_fallback.is_empty() {
return res;
}
match res {
Ok(true) => Ok(true),
_ => download(client.clone(), &format!("{}{}/{}", &server_fallback, &hash, &server_file), &local_file).await
}
}
async fn download(client: reqwest::Client, url: &str, file: &Path) -> Result<bool, structs::Error> {
let resp = client.get(url)
.send()
.await?;
if resp.status() == 404 {
return Ok(false);
}
resp.error_for_status_ref()?;
let bytes = resp.bytes().await?;
write(&file, bytes)?;
Ok(true)
}