Compare commits
No commits in common. "acea5d26dfdcaa4001872dddb51fd82a88f77279" and "7d2065d5f17eb6506c7151dddb7e07384cbd1d3a" have entirely different histories.
acea5d26df
...
7d2065d5f1
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "nhentairs"
|
name = "nhentairs"
|
||||||
version = "0.6.0"
|
version = "0.5.6"
|
||||||
authors = ["blank X <theblankx@protonmail.com>"]
|
authors = ["blank X <theblankx@protonmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
@ -13,5 +13,4 @@ lto = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
tokio = { version = "1.33", features = ["rt-multi-thread", "sync", "time"] }
|
tokio = { version = "1.0", features = ["rt-multi-thread", "sync", "time"] }
|
||||||
quick-xml = "0.31"
|
|
||||||
|
|
113
src/api.rs
113
src/api.rs
|
@ -1,113 +0,0 @@
|
||||||
use crate::structs;
|
|
||||||
|
|
||||||
use quick_xml::events::Event;
|
|
||||||
use quick_xml::Reader;
|
|
||||||
use std::env;
|
|
||||||
use std::process::exit;
|
|
||||||
|
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
pub fn get_client() -> reqwest::Client {
|
|
||||||
let mut builder = reqwest::Client::builder();
|
|
||||||
match env::var("NHENTAIRS_INSECURE") {
|
|
||||||
Ok(val) => {
|
|
||||||
if val == "true" || val == "yes" || val == "1" {
|
|
||||||
builder = builder.danger_accept_invalid_certs(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(env::VarError::NotPresent) => {}
|
|
||||||
Err(err) => eprintln!("failed to parse NHENTAIRS_INSECURE: {err}"),
|
|
||||||
};
|
|
||||||
match builder.build() {
|
|
||||||
Ok(client) => client,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Failed to create reqwest client: {err}");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sauce_info(
|
|
||||||
client: reqwest::Client,
|
|
||||||
sauce: i32,
|
|
||||||
) -> Result<structs::GalleryInfo, structs::Error> {
|
|
||||||
let mut url = String::from("https://nhentai.net/api/gallery/");
|
|
||||||
url.push_str(&sauce.to_string());
|
|
||||||
let resp = client.get(&url).send().await?;
|
|
||||||
Ok(serde_json::from_str(&resp.text().await?)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_related_galleries(
|
|
||||||
client: &reqwest::Client,
|
|
||||||
sauce: i32,
|
|
||||||
) -> Result<structs::RelatedGalleries, structs::Error> {
|
|
||||||
let mut url = String::from("https://nhentai.net/api/gallery/");
|
|
||||||
url.push_str(&sauce.to_string());
|
|
||||||
url.push_str("/related");
|
|
||||||
let resp = client.get(&url).send().await?;
|
|
||||||
Ok(serde_json::from_str(&resp.text().await?)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_search_info(
|
|
||||||
client: reqwest::Client,
|
|
||||||
search_query: &str,
|
|
||||||
) -> Result<Vec<structs::MiniGalleryInfo>, structs::Error> {
|
|
||||||
let resp = client
|
|
||||||
.get("https://nhentai.net/search/")
|
|
||||||
.query(&[("q", search_query)])
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let text = resp.text().await?;
|
|
||||||
let mut results = Vec::new();
|
|
||||||
let mut gallery_info = structs::MiniGalleryInfo {
|
|
||||||
id: 0,
|
|
||||||
title: "".to_string(),
|
|
||||||
};
|
|
||||||
let mut reading_gallery = false;
|
|
||||||
|
|
||||||
let mut reader = Reader::from_str(&text);
|
|
||||||
reader.trim_text(true).check_end_names(false);
|
|
||||||
loop {
|
|
||||||
match reader.read_event() {
|
|
||||||
Ok(Event::Start(e)) if e.local_name().as_ref() == "a".as_bytes() => {
|
|
||||||
let class_attribute = match e.try_get_attribute("class")? {
|
|
||||||
Some(a) => a,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
if class_attribute.decode_and_unescape_value(&reader)? != "cover" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let href_attribute = match e.try_get_attribute("href")? {
|
|
||||||
Some(a) => a,
|
|
||||||
None => return Err(structs::Error::Unknown("failed to find href in <a>")),
|
|
||||||
};
|
|
||||||
let href = href_attribute.decode_and_unescape_value(&reader)?;
|
|
||||||
let id_str = match href.split('/').nth(2) {
|
|
||||||
Some(i) => i,
|
|
||||||
None => return Err(structs::Error::Unknown("failed to find id in <a href>")),
|
|
||||||
};
|
|
||||||
|
|
||||||
reading_gallery = true;
|
|
||||||
gallery_info.id = id_str.parse()?;
|
|
||||||
}
|
|
||||||
Ok(Event::Text(e)) if reading_gallery => {
|
|
||||||
gallery_info.title.push_str(&e.unescape()?);
|
|
||||||
}
|
|
||||||
Ok(Event::End(e)) if reading_gallery && e.local_name().as_ref() == "a".as_bytes() => {
|
|
||||||
results.push(gallery_info);
|
|
||||||
reading_gallery = false;
|
|
||||||
gallery_info = structs::MiniGalleryInfo {
|
|
||||||
id: 0,
|
|
||||||
title: "".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(Event::Eof) => break,
|
|
||||||
// why cast? i have no idea, the compiler just doesn't see the From
|
|
||||||
Err(err) => return Err(structs::Error::QuickXML(err)),
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
mod download;
|
|
||||||
mod related;
|
|
||||||
mod search;
|
|
||||||
mod view;
|
mod view;
|
||||||
|
mod search;
|
||||||
|
mod download;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -12,24 +11,19 @@ pub async fn run() {
|
||||||
let path = args.next().expect("Cannot get binary path");
|
let path = args.next().expect("Cannot get binary path");
|
||||||
let path = Path::new(&path).file_stem().unwrap().to_str().unwrap();
|
let path = Path::new(&path).file_stem().unwrap().to_str().unwrap();
|
||||||
let operation = match args.next() {
|
let operation = match args.next() {
|
||||||
Some(operation) => operation,
|
Some(operation) => operation,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Missing operation, run `{} help`", path);
|
eprintln!("Missing operation, run `{} help`", path);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match operation.as_str() {
|
match operation.as_str() {
|
||||||
"search" => search::run(args).await,
|
"search" => search::run(args).await,
|
||||||
"view" | "show" | "info" => view::run(args).await,
|
"view" | "show" | "info" => view::run(args).await,
|
||||||
"related" => related::run(args).await,
|
|
||||||
"download" | "dl" => download::run(args).await,
|
"download" | "dl" => download::run(args).await,
|
||||||
"help" => println!(
|
"help" => println!(r#"Usage: {} search QUERY
|
||||||
r#"Usage: {} search <query>
|
or {} info/view/show SAUCE [SAUCE]...
|
||||||
or {} info/view/show <sauce>...
|
or {} download/dl SAUCE [SAUCE]..."#, path, path, path),
|
||||||
or {} related <sauce>...
|
|
||||||
or {} download/dl <sauce>..."#,
|
|
||||||
path, path, path, path
|
|
||||||
),
|
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown operation, run `{} help`", path);
|
eprintln!("Unknown operation, run `{} help`", path);
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -1,38 +1,37 @@
|
||||||
use crate::api;
|
|
||||||
use crate::structs;
|
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
use crate::structs;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
|
||||||
use std::fs::{create_dir, rename, write};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::exit;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
extern crate reqwest;
|
use std::fs::{rename, create_dir, write};
|
||||||
extern crate tokio;
|
extern crate tokio;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
const DOWNLOAD_WORKERS: usize = 5;
|
const DOWNLOAD_WORKERS: usize = 5;
|
||||||
const FAIL_DOWNLOAD_WAIT_TIME: u64 = 5000;
|
const FAIL_DOWNLOAD_WAIT_TIME: u64 = 5000;
|
||||||
|
|
||||||
pub async fn run(args: env::Args) {
|
pub async fn run(args: env::Args) {
|
||||||
let sauces = utils::get_arg_sauces(args);
|
let sauces = utils::get_arg_sauces(args).unwrap();
|
||||||
let client = api::get_client();
|
if sauces.len() < 1 {
|
||||||
|
eprintln!("Missing sauce(s)");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::new();
|
||||||
let mut pages_vec: Vec<(String, String)> = Vec::new();
|
let mut pages_vec: Vec<(String, String)> = Vec::new();
|
||||||
{
|
{
|
||||||
let mut handles: Vec<JoinHandle<structs::GalleryInfoSuccess>> =
|
let mut handles: Vec<JoinHandle<structs::GalleryInfoSuccess>> = Vec::with_capacity(sauces.len());
|
||||||
Vec::with_capacity(sauces.len());
|
|
||||||
let mut sauce_info_vec: Vec<structs::GalleryInfoSuccess> = Vec::with_capacity(sauces.len());
|
let mut sauce_info_vec: Vec<structs::GalleryInfoSuccess> = Vec::with_capacity(sauces.len());
|
||||||
for sauce in sauces {
|
for sauce in sauces {
|
||||||
let cloned_client = client.clone();
|
let cloned_client = client.clone();
|
||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
match api::get_sauce_info(cloned_client, sauce).await.unwrap() {
|
match utils::get_sauce_info(cloned_client, sauce).await.unwrap() {
|
||||||
structs::GalleryInfo::Info(sauce_info) => sauce_info,
|
structs::GalleryInfo::Info(sauce_info) => sauce_info,
|
||||||
structs::GalleryInfo::Error(sauce_error) => {
|
structs::GalleryInfo::Error(sauce_error) => panic!("{} returned: {}", sauce, sauce_error.error)
|
||||||
panic!("{} returned: {}", sauce, sauce_error.error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -46,8 +45,8 @@ pub async fn run(args: env::Args) {
|
||||||
Ok(()) => write(base_path.join("info.txt"), format!("{}\n", &sauce_info)).unwrap(),
|
Ok(()) => write(base_path.join("info.txt"), format!("{}\n", &sauce_info)).unwrap(),
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind() {
|
||||||
std::io::ErrorKind::AlreadyExists => (),
|
std::io::ErrorKind::AlreadyExists => (),
|
||||||
_ => panic!("Got a weird error while creating dir: {}", err),
|
_ => panic!("Got a weird error while creating dir: {}", err)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
let mut page_num: i32 = 1;
|
let mut page_num: i32 = 1;
|
||||||
for page in sauce_info.images.pages {
|
for page in sauce_info.images.pages {
|
||||||
|
@ -55,7 +54,7 @@ pub async fn run(args: env::Args) {
|
||||||
"j" => ".jpg",
|
"j" => ".jpg",
|
||||||
"p" => ".png",
|
"p" => ".png",
|
||||||
"g" => ".gif",
|
"g" => ".gif",
|
||||||
_ => panic!("Unknown extension type: {}", page.t),
|
_ => panic!("Unknown extension type: {}", page.t)
|
||||||
};
|
};
|
||||||
let mut file_name = page_num.to_string();
|
let mut file_name = page_num.to_string();
|
||||||
file_name.push_str(file_ext);
|
file_name.push_str(file_ext);
|
||||||
|
@ -63,10 +62,9 @@ pub async fn run(args: env::Args) {
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
pages_vec.push((
|
pages_vec.push((
|
||||||
String::from(file_path.to_str().unwrap()),
|
String::from(file_path.to_str().unwrap()),
|
||||||
format!(
|
format!("https://i.nhentai.net/galleries/{}/{}",
|
||||||
"https://i.nhentai.net/galleries/{}/{}",
|
sauce_info.media_id,
|
||||||
sauce_info.media_id, file_name
|
file_name)
|
||||||
),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
page_num += 1;
|
page_num += 1;
|
||||||
|
@ -95,16 +93,13 @@ pub async fn run(args: env::Args) {
|
||||||
let mut tmp_file_path = file_path.clone();
|
let mut tmp_file_path = file_path.clone();
|
||||||
tmp_file_path.push_str(".tmp");
|
tmp_file_path.push_str(".tmp");
|
||||||
loop {
|
loop {
|
||||||
match download_file(cloned_client.clone(), &url, &tmp_file_path).await {
|
match utils::download_file(cloned_client.clone(), &url, &tmp_file_path).await {
|
||||||
Ok(success) => {
|
Ok(success) => {
|
||||||
if success {
|
if success {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Err(err) => eprintln!(
|
Err(err) => eprintln!("[DW{}] Failed to download {} due to {}, sleeping for {}ms", worker_id, file_path, err, FAIL_DOWNLOAD_WAIT_TIME)
|
||||||
"[DW{}] Failed to download {} due to {}, sleeping for {}ms",
|
|
||||||
worker_id, file_path, err, FAIL_DOWNLOAD_WAIT_TIME
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
sleep(Duration::from_millis(FAIL_DOWNLOAD_WAIT_TIME)).await;
|
sleep(Duration::from_millis(FAIL_DOWNLOAD_WAIT_TIME)).await;
|
||||||
}
|
}
|
||||||
|
@ -117,20 +112,3 @@ pub async fn run(args: env::Args) {
|
||||||
handle.await.unwrap();
|
handle.await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_file(
|
|
||||||
client: reqwest::Client,
|
|
||||||
url: &str,
|
|
||||||
file_name: &str,
|
|
||||||
) -> Result<bool, reqwest::Error> {
|
|
||||||
let resp = client.get(url).send().await?;
|
|
||||||
Ok(match resp.headers().get("Content-Type") {
|
|
||||||
Some(header) if header.to_str().unwrap_or_default().starts_with("image/") => {
|
|
||||||
let bytes = resp.bytes().await?;
|
|
||||||
let mut file = File::create(&file_name).unwrap();
|
|
||||||
file.write_all(&bytes).unwrap();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
use crate::api;
|
|
||||||
use crate::structs;
|
|
||||||
use crate::utils;
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::process::exit;
|
|
||||||
extern crate reqwest;
|
|
||||||
extern crate tokio;
|
|
||||||
|
|
||||||
pub async fn run(args: env::Args) {
|
|
||||||
let sauces = utils::get_arg_sauces(args);
|
|
||||||
let is_multi = sauces.len() > 1;
|
|
||||||
|
|
||||||
let client = api::get_client();
|
|
||||||
let mut failures = 0;
|
|
||||||
let mut one_done = false;
|
|
||||||
|
|
||||||
for sauce in sauces {
|
|
||||||
let sauce_info = api::get_related_galleries(&client, sauce).await;
|
|
||||||
match sauce_info {
|
|
||||||
Ok(structs::RelatedGalleries::Galleries(related_galleries)) => {
|
|
||||||
show_related_galleries(sauce, &related_galleries, one_done, is_multi)
|
|
||||||
}
|
|
||||||
Ok(structs::RelatedGalleries::Error(err)) => {
|
|
||||||
show_error(sauce, &err.error, one_done, true);
|
|
||||||
failures += 1;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
show_error(sauce, &err, one_done, is_multi);
|
|
||||||
failures += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
one_done = true;
|
|
||||||
}
|
|
||||||
exit(failures);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_related_galleries(
|
|
||||||
sauce: i32,
|
|
||||||
related_galleries: &structs::RelatedGalleriesSuccess,
|
|
||||||
prepend_newline: bool,
|
|
||||||
is_multi: bool,
|
|
||||||
) {
|
|
||||||
if prepend_newline {
|
|
||||||
println!("");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prefix = "";
|
|
||||||
if is_multi {
|
|
||||||
println!("{}:", sauce);
|
|
||||||
prefix = "- ";
|
|
||||||
}
|
|
||||||
for i in &related_galleries.result {
|
|
||||||
let title = i.title.english.as_deref().or(i.title.japanese.as_deref());
|
|
||||||
println!("{}{}: {}", prefix, i.id, title.unwrap_or("<unknown title>"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_error<T: std::fmt::Display>(
|
|
||||||
sauce: i32,
|
|
||||||
error: &T,
|
|
||||||
prepend_newline: bool,
|
|
||||||
prepend_sauce: bool,
|
|
||||||
) {
|
|
||||||
if prepend_newline {
|
|
||||||
eprintln!("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !prepend_sauce {
|
|
||||||
eprintln!("{}", error);
|
|
||||||
} else {
|
|
||||||
eprintln!("{}: {}", sauce, error);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::api;
|
use crate::utils;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
@ -14,14 +14,16 @@ pub async fn run(args: env::Args) {
|
||||||
eprintln!("Missing search query");
|
eprintln!("Missing search query");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
let search_info = api::get_search_info(api::get_client(), &query)
|
let search_info = utils::get_search_info(reqwest::Client::new(), &query).await.unwrap();
|
||||||
.await
|
if search_info.num_pages < 1 {
|
||||||
.unwrap();
|
|
||||||
if search_info.len() < 1 {
|
|
||||||
eprintln!("No results found");
|
eprintln!("No results found");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
for result in search_info {
|
for result in search_info.result {
|
||||||
println!("{}: {}", result.id, result.title);
|
let mut title = &result.title.english.unwrap_or_default();
|
||||||
|
if title == "" {
|
||||||
|
title = &result.title.japanese.as_ref().unwrap();
|
||||||
|
}
|
||||||
|
println!("{}: {}", result.id, &title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
use crate::api;
|
|
||||||
use crate::structs;
|
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
|
use crate::structs;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
extern crate reqwest;
|
|
||||||
extern crate tokio;
|
extern crate tokio;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
pub async fn run(args: env::Args) {
|
pub async fn run(args: env::Args) {
|
||||||
let sauces = utils::get_arg_sauces(args);
|
let sauces = utils::get_arg_sauces(args).unwrap();
|
||||||
let client = api::get_client();
|
if sauces.len() < 1 {
|
||||||
let mut handles: Vec<JoinHandle<(structs::GalleryInfo, i32)>> =
|
eprintln!("Missing sauce(s)");
|
||||||
Vec::with_capacity(sauces.len());
|
exit(1);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut handles: Vec<JoinHandle<(structs::GalleryInfo, i32)>> = Vec::with_capacity(sauces.len());
|
||||||
for sauce in sauces {
|
for sauce in sauces {
|
||||||
let cloned_client = client.clone();
|
let cloned_client = client.clone();
|
||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
(
|
(utils::get_sauce_info(cloned_client, sauce).await.unwrap(), sauce)
|
||||||
api::get_sauce_info(cloned_client, sauce).await.unwrap(),
|
|
||||||
sauce,
|
|
||||||
)
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
let mut fail = false;
|
let mut fail = false;
|
||||||
|
@ -32,7 +31,7 @@ pub async fn run(args: env::Args) {
|
||||||
println!("");
|
println!("");
|
||||||
}
|
}
|
||||||
println!("{}", &sauce_info);
|
println!("{}", &sauce_info);
|
||||||
}
|
},
|
||||||
structs::GalleryInfo::Error(sauce_error) => {
|
structs::GalleryInfo::Error(sauce_error) => {
|
||||||
if one_done {
|
if one_done {
|
||||||
eprintln!("");
|
eprintln!("");
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
mod api;
|
|
||||||
mod commands;
|
|
||||||
mod structs;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod structs;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
extern crate tokio;
|
extern crate tokio;
|
||||||
|
|
||||||
|
@ -9,6 +8,6 @@ fn main() {
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to build tokio runtime")
|
.unwrap()
|
||||||
.block_on(commands::run());
|
.block_on(commands::run());
|
||||||
}
|
}
|
||||||
|
|
126
src/structs.rs
126
src/structs.rs
|
@ -1,14 +1,8 @@
|
||||||
use serde::de::{self, Visitor};
|
|
||||||
use serde::{Deserialize, Deserializer};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::num::ParseIntError;
|
use std::collections::BTreeMap;
|
||||||
|
use serde::de::{self, Visitor};
|
||||||
#[derive(Deserialize, Debug)]
|
use serde::{Deserialize, Deserializer};
|
||||||
pub struct APIError {
|
|
||||||
pub error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct GalleryTitleInfo {
|
pub struct GalleryTitleInfo {
|
||||||
|
@ -21,14 +15,14 @@ pub struct GalleryTitleInfo {
|
||||||
pub struct GalleryImageInfo {
|
pub struct GalleryImageInfo {
|
||||||
pub t: String,
|
pub t: String,
|
||||||
pub w: i32,
|
pub w: i32,
|
||||||
pub h: i32,
|
pub h: i32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct GalleryImagesInfo {
|
pub struct GalleryImagesInfo {
|
||||||
pub pages: Vec<GalleryImageInfo>,
|
pub pages: Vec<GalleryImageInfo>,
|
||||||
pub cover: GalleryImageInfo,
|
pub cover: GalleryImageInfo,
|
||||||
pub thumbnail: GalleryImageInfo,
|
pub thumbnail: GalleryImageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@ -37,7 +31,7 @@ pub struct GalleryTagInfo {
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub count: i32,
|
pub count: i32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@ -51,32 +45,26 @@ pub struct GalleryInfoSuccess {
|
||||||
pub upload_date: i32,
|
pub upload_date: i32,
|
||||||
pub tags: Vec<GalleryTagInfo>,
|
pub tags: Vec<GalleryTagInfo>,
|
||||||
pub num_pages: i32,
|
pub num_pages: i32,
|
||||||
pub num_favorites: i32,
|
pub num_favorites: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryInfoError {
|
||||||
|
pub error: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum GalleryInfo {
|
pub enum GalleryInfo {
|
||||||
Info(GalleryInfoSuccess),
|
Info(GalleryInfoSuccess),
|
||||||
Error(APIError),
|
Error(GalleryInfoError)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct RelatedGalleriesSuccess {
|
pub struct SearchInfo {
|
||||||
pub result: Vec<GalleryInfoSuccess>,
|
pub result: Vec<GalleryInfoSuccess>,
|
||||||
}
|
pub num_pages: i32,
|
||||||
|
pub per_page: i32
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum RelatedGalleries {
|
|
||||||
Galleries(RelatedGalleriesSuccess),
|
|
||||||
Error(APIError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MiniGalleryInfo {
|
|
||||||
pub id: i32,
|
|
||||||
pub title: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for GalleryInfoSuccess {
|
impl fmt::Display for GalleryInfoSuccess {
|
||||||
|
@ -108,75 +96,23 @@ impl fmt::Display for GalleryInfoSuccess {
|
||||||
"group" => "Groups",
|
"group" => "Groups",
|
||||||
"language" => "Languages",
|
"language" => "Languages",
|
||||||
"category" => "Categories",
|
"category" => "Categories",
|
||||||
_ => tag_key,
|
_ => tag_key
|
||||||
};
|
};
|
||||||
text.push_str(&format!("\n{}: {}", tag_key, tag_value.join(", ")));
|
text.push_str(&format!("\n{}: {}", tag_key, tag_value.join(", ")));
|
||||||
}
|
}
|
||||||
text.push_str(&format!(
|
text.push_str(&format!("\nPages: {}\nFavorites: {}", self.num_pages, self.num_favorites));
|
||||||
"\nPages: {}\nFavorites: {}",
|
|
||||||
self.num_pages, self.num_favorites
|
|
||||||
));
|
|
||||||
formatter.write_str(&text)
|
formatter.write_str(&text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
Reqwest(reqwest::Error),
|
|
||||||
SerdeJSON(serde_json::Error),
|
|
||||||
QuickXML(quick_xml::Error),
|
|
||||||
ParseInt(ParseIntError),
|
|
||||||
Unknown(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
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 From<quick_xml::Error> for Error {
|
|
||||||
#[inline]
|
|
||||||
fn from(error: quick_xml::Error) -> Error {
|
|
||||||
Error::QuickXML(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParseIntError> for Error {
|
|
||||||
#[inline]
|
|
||||||
fn from(error: ParseIntError) -> Error {
|
|
||||||
Error::ParseInt(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
let str = match self {
|
|
||||||
Error::Reqwest(err) => format!("reqwest error: {}", err),
|
|
||||||
Error::SerdeJSON(err) => format!("serde_json error: {}", err),
|
|
||||||
Error::QuickXML(err) => format!("quick_xml error: {}", err),
|
|
||||||
Error::ParseInt(err) => format!("parse int error: {}", err),
|
|
||||||
Error::Unknown(err) => err.to_string(),
|
|
||||||
};
|
|
||||||
formatter.write_str(&str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_to_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
|
fn convert_to_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>
|
||||||
{
|
{
|
||||||
struct ConvertToI32<T>(PhantomData<fn() -> T>);
|
struct ConvertToI32<T>(PhantomData<fn() -> T>);
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for ConvertToI32<i32> {
|
impl<'de> Visitor<'de> for ConvertToI32<i32>
|
||||||
|
{
|
||||||
type Value = i32;
|
type Value = i32;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -185,28 +121,28 @@ where
|
||||||
|
|
||||||
fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E>
|
fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
Ok(i32::from(value))
|
Ok(i32::from(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_i16<E>(self, value: i16) -> Result<Self::Value, E>
|
fn visit_i16<E>(self, value: i16) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
Ok(i32::from(value))
|
Ok(i32::from(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
|
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
use std::i32;
|
use std::i32;
|
||||||
if value >= i64::from(i32::MIN) && value <= i64::from(i32::MAX) {
|
if value >= i64::from(i32::MIN) && value <= i64::from(i32::MAX) {
|
||||||
|
@ -218,21 +154,21 @@ where
|
||||||
|
|
||||||
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
|
fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
Ok(i32::from(value))
|
Ok(i32::from(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
|
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
Ok(i32::from(value))
|
Ok(i32::from(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
|
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
use std::{i32, u32};
|
use std::{i32, u32};
|
||||||
if value <= i32::MAX as u32 {
|
if value <= i32::MAX as u32 {
|
||||||
|
@ -244,7 +180,7 @@ where
|
||||||
|
|
||||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
use std::{i32, u64};
|
use std::{i32, u64};
|
||||||
if value <= i32::MAX as u64 {
|
if value <= i32::MAX as u64 {
|
||||||
|
@ -256,9 +192,9 @@ where
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error
|
||||||
{
|
{
|
||||||
// https://brokenco.de/2020/08/03/serde-deserialize-with-string.html
|
// https://brokenco.de/2020/08/03/serde-deserialize-with-string.html
|
||||||
value.parse::<i32>().map_err(serde::de::Error::custom)
|
value.parse::<i32>().map_err(serde::de::Error::custom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
src/utils.rs
58
src/utils.rs
|
@ -1,26 +1,56 @@
|
||||||
|
use crate::structs;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
use std::process::exit;
|
pub async fn get_sauce_info(client: reqwest::Client, sauce: i32) -> Result<structs::GalleryInfo, reqwest::Error> {
|
||||||
|
let mut uri = String::from("https://nhentai.net/api/gallery/");
|
||||||
|
uri.push_str(&sauce.to_string());
|
||||||
|
let resp = client.get(&uri)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(serde_json::from_str(&resp.text().await?).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_arg_sauces(args: env::Args) -> Vec<i32> {
|
pub async fn get_search_info(client: reqwest::Client, search_query: &str) -> Result<structs::SearchInfo, reqwest::Error> {
|
||||||
|
let uri = "https://nhentai.net/api/galleries/search";
|
||||||
|
let resp = client.get(uri)
|
||||||
|
.query(&[("query", search_query)])
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(serde_json::from_str(&resp.text().await?).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_file(client: reqwest::Client, url: &str, file_name: &str) -> Result<bool, reqwest::Error> {
|
||||||
|
let resp = client.get(url)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(match resp.headers().get("Content-Type") {
|
||||||
|
Some(header) if header.to_str().unwrap_or_default().starts_with("image/") => {
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
let mut file = File::create(&file_name).unwrap();
|
||||||
|
file.write_all(&bytes).unwrap();
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_arg_sauces(args: env::Args) -> Result<Vec<i32>, String> {
|
||||||
let mut sauces: Vec<i32> = Vec::new();
|
let mut sauces: Vec<i32> = Vec::new();
|
||||||
|
|
||||||
for sauce in args {
|
for sauce in args {
|
||||||
let sauce: i32 = match sauce.parse() {
|
let sauce: i32 = match sauce.parse() {
|
||||||
Ok(sauce) => sauce,
|
Ok(sauce) => sauce,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!("{} is not a number/sauce", sauce);
|
return Err(format!("{} is not a number/sauce", sauce));
|
||||||
exit(1);
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if !sauces.contains(&sauce) {
|
if !sauces.contains(&sauce) {
|
||||||
sauces.push(sauce);
|
sauces.push(sauce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(sauces)
|
||||||
if sauces.len() < 1 {
|
|
||||||
eprintln!("Missing sauce(s)");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
sauces
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue