Compare commits

..

No commits in common. "acea5d26dfdcaa4001872dddb51fd82a88f77279" and "7d2065d5f17eb6506c7151dddb7e07384cbd1d3a" have entirely different histories.

11 changed files with 391 additions and 789 deletions

664
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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)
}

View File

@ -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;
@ -21,15 +20,10 @@ pub async fn run() {
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)

View File

@ -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,
})
}

View File

@ -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);
}
}

View File

@ -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);
} }
} }

View File

@ -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!("");

View File

@ -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());
} }

View File

@ -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,7 +192,7 @@ 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)

View File

@ -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
} }