use std::fs::{create_dir, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use clap::Parser;
use reqwest::Url;
use uuid::Uuid;
use mangadex_api::v5::MangaDexClient;
use mangadex_api::HttpClientRef;
use mangadex_api_types::RelationshipType;
#[derive(Parser, Debug)]
#[clap(
name = "Manga Chapter Downloader",
about = "Fetch the pages for a chapter."
)]
struct Args {
#[clap()]
chapter_id: Uuid,
#[clap(long)]
data_saver: bool,
#[clap(short, long = "download")]
output: Option<PathBuf>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if let Err(e) = run(args).await {
use std::process;
eprintln!("Application error: {e}");
process::exit(1);
}
}
async fn run(args: Args) -> anyhow::Result<()> {
let client = MangaDexClient::default();
let mut output = args.output.clone().unwrap_or_default();
let chapter = client
.chapter()
.id(args.chapter_id)
.get()
.build()?
.send()
.await?;
let mut scanlation_groups = Vec::new();
for r in &chapter.data.relationships {
if r.type_ == RelationshipType::ScanlationGroup {
let group = client
.scanlation_group()
.id(r.id)
.get()
.build()?
.send()
.await?;
scanlation_groups.push(group.data.attributes.name);
}
}
let mut users = Vec::new();
if scanlation_groups.is_empty() {
for r in &chapter.data.relationships {
if r.type_ == RelationshipType::User {
let user = client.user().id(r.id).get().build()?.send().await?;
users.push(user.data.attributes.username);
}
}
}
let uploader = if !scanlation_groups.is_empty() {
scanlation_groups.join(" & ")
} else {
users.join(" & ")
};
let volume_number = match chapter.data.attributes.volume {
Some(v) => format!("Vol.{v} "),
None => "".to_string(),
};
let chapter_number = match chapter.data.attributes.chapter {
Some(c) => format!("Ch.{c} "),
None => "".to_string(),
};
let title_separator = if (volume_number.is_empty() && chapter_number.is_empty())
|| chapter
.data
.attributes
.title
.as_ref()
.map(|t| t.is_empty())
.unwrap_or(false)
{
""
} else {
"- "
};
output.push(format!(
"{uploader}_{volume}{chapter}{separator}{title}",
uploader = uploader,
volume = volume_number,
chapter = chapter_number,
separator = title_separator,
title = chapter
.data
.attributes
.title
.clone()
.unwrap_or(String::default())
));
if args.output.is_some() && !output.is_dir() {
println!("Created {:?}", &output);
create_dir(&output)?;
}
let at_home = client
.at_home()
.server()
.id(args.chapter_id)
.get()
.build()?
.send()
.await?;
let page_filenames = if !args.data_saver {
at_home.body.chapter.data
} else {
at_home.body.chapter.data_saver
};
for (i, server_filename) in page_filenames.iter().enumerate() {
let path = Path::new(server_filename);
let ext = match path.extension() {
Some(e) => format!(".{}", e.to_str().unwrap_or("")),
None => "".to_string(),
};
let filename = format!("{:03}{}", i + 1, ext);
let page_url = at_home
.body
.base_url
.join(&format!(
"/{quality_mode}/{chapter_hash}/{page_filename}",
quality_mode = if args.data_saver {
"data-saver"
} else {
"data"
},
chapter_hash = at_home.body.chapter.hash,
page_filename = server_filename
))
.unwrap();
if args.output.is_some() {
print!("Downloading {}...", &filename);
std::io::stdout().flush()?;
download_file(
client.get_http_client().clone(),
&page_url,
output.as_path(),
&filename,
)
.await?;
println!("done");
} else {
let page_res = client
.get_http_client()
.read()
.await
.client
.get(page_url.clone())
.send()
.await?
.bytes()
.await?;
println!("{filename:?} - {page_res:#?}");
}
}
Ok(())
}
async fn download_file(
http_client: HttpClientRef,
url: &Url,
output: &Path,
file_name: &str,
) -> anyhow::Result<()> {
let image_bytes = http_client
.read()
.await
.client
.get(url.clone())
.send()
.await?
.bytes()
.await?;
let mut file_buffer = File::create(output.join(file_name))?;
file_buffer.write_all(&image_bytes)?;
Ok(())
}