Initial commit
This commit is contained in:
commit
c5dab76260
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "nhentairs"
|
||||||
|
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]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
reqwest = { version = "0.10", features = ["stream"] }
|
||||||
|
tokio = { version = "0.2", features = ["rt-core", "macros", "sync"] }
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 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.
|
|
@ -0,0 +1,32 @@
|
||||||
|
mod view;
|
||||||
|
mod search;
|
||||||
|
mod download;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
pub async fn run() {
|
||||||
|
let mut args = env::args();
|
||||||
|
let path = args.next().expect("Cannot get binary path");
|
||||||
|
let path = Path::new(&path).file_stem().unwrap().to_str().unwrap();
|
||||||
|
let operation = match args.next() {
|
||||||
|
Some(operation) => operation,
|
||||||
|
None => {
|
||||||
|
eprintln!("Missing operation, run `{} help`", path);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match operation.as_str() {
|
||||||
|
"search" => search::run(args).await,
|
||||||
|
"view" | "show" | "info" => view::run(args).await,
|
||||||
|
"download" | "dl" => download::run(args).await,
|
||||||
|
"help" => println!(r#"Usage: {} search QUERY
|
||||||
|
or {} info/view/show SAUCE [SAUCE]...
|
||||||
|
or {} download/dl SAUCE [SAUCE]..."#, path, path, path),
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown operation, run `{} help`", path);
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::exit;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use std::fs::{rename, create_dir};
|
||||||
|
extern crate tokio;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
|
const DOWNLOAD_WORKERS: usize = 5;
|
||||||
|
|
||||||
|
pub async fn run(args: env::Args) {
|
||||||
|
let sauces = utils::get_arg_sauces(args).unwrap();
|
||||||
|
if sauces.len() < 1 {
|
||||||
|
eprintln!("Missing sauce(s)");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut handles: Vec<JoinHandle<()>> = Vec::new();
|
||||||
|
let mut hashm = HashMap::new();
|
||||||
|
let (tx, mut rx) = mpsc::channel(100);
|
||||||
|
for sauce in sauces {
|
||||||
|
let cloned_client = client.clone();
|
||||||
|
let mut cloned_tx = tx.clone();
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
let sauce_info = utils::get_sauce_info(cloned_client, sauce).await.unwrap();
|
||||||
|
let base_path = sauce_info.id.to_string();
|
||||||
|
let base_path = Path::new(&base_path);
|
||||||
|
match create_dir(base_path) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(err) => match err.kind() {
|
||||||
|
std::io::ErrorKind::AlreadyExists => (),
|
||||||
|
_ => panic!("Got a weird error while creating dir: {}", err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut page_num: usize = 1;
|
||||||
|
for page in sauce_info.images.pages {
|
||||||
|
let file_ext = match page.t.as_str() {
|
||||||
|
"j" => ".jpg",
|
||||||
|
"p" => ".png",
|
||||||
|
"g" => ".gif",
|
||||||
|
_ => panic!("Unknown extension type: {}", page.t)
|
||||||
|
};
|
||||||
|
let mut file_name = page_num.to_string();
|
||||||
|
file_name.push_str(file_ext);
|
||||||
|
let file_path = base_path.join(&file_name);
|
||||||
|
if !file_path.exists() {
|
||||||
|
cloned_tx.send((
|
||||||
|
String::from(file_path.to_str().unwrap()),
|
||||||
|
format!("https://i.nhentai.net/galleries/{}/{}",
|
||||||
|
sauce_info.media_id,
|
||||||
|
file_name)
|
||||||
|
)).await.unwrap();
|
||||||
|
}
|
||||||
|
page_num += 1;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
drop(tx);
|
||||||
|
while let Some((file_path, url)) = rx.recv().await {
|
||||||
|
hashm.insert(file_path, url);
|
||||||
|
}
|
||||||
|
for handle in handles {
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handles = Vec::with_capacity(5);
|
||||||
|
let (tx, mut rx) = mpsc::channel(5);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(ntx) = rx.recv().await {
|
||||||
|
let ntx: oneshot::Sender<(String, String)> = ntx;
|
||||||
|
ntx.send(match hashm.iter().next() {
|
||||||
|
Some((key, value)) => {
|
||||||
|
let key = key.to_string();
|
||||||
|
let value = value.to_string();
|
||||||
|
hashm.remove(&key).unwrap();
|
||||||
|
(key, value)
|
||||||
|
},
|
||||||
|
None => (String::new(), String::new()),
|
||||||
|
}).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for worker_id in 0..DOWNLOAD_WORKERS {
|
||||||
|
let tcloned_client = client.clone();
|
||||||
|
let mut cloned_tx = tx.clone();
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
println!("[DW{}] Up!", worker_id);
|
||||||
|
loop {
|
||||||
|
let cloned_client = tcloned_client.clone();
|
||||||
|
let (ntx, nrx) = oneshot::channel();
|
||||||
|
cloned_tx.send(ntx).await.unwrap();
|
||||||
|
let (file_path, url) = nrx.await.unwrap();
|
||||||
|
if file_path.is_empty() && url.is_empty() {
|
||||||
|
println!("[DW{}] Down!", worker_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
println!("[DW{}] Downloading {} to {}", worker_id, url, file_path);
|
||||||
|
let mut tmp_file_path = String::from(&file_path);
|
||||||
|
tmp_file_path.push_str(".tmp");
|
||||||
|
utils::download_file(cloned_client, &url, &tmp_file_path).await.unwrap();
|
||||||
|
rename(&tmp_file_path, &file_path).unwrap();
|
||||||
|
println!("[DW{}] {} downloaded", worker_id, file_path);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
drop(tx);
|
||||||
|
for handle in handles {
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::process::exit;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
|
pub async fn run(args: env::Args) {
|
||||||
|
let mut query = String::new();
|
||||||
|
for arg in args {
|
||||||
|
query.push_str(&format!(" {}", arg));
|
||||||
|
}
|
||||||
|
if query.len() < 1 {
|
||||||
|
eprintln!("Missing search query");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let search_info = utils::get_search_info(client.clone(), &query[1..]).await.unwrap();
|
||||||
|
if search_info.num_pages < 1 {
|
||||||
|
eprintln!("No results found");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
for result in search_info.result {
|
||||||
|
let text;
|
||||||
|
if result.title.english != "" {
|
||||||
|
text = result.title.english;
|
||||||
|
} else {
|
||||||
|
text = result.title.japanese;
|
||||||
|
}
|
||||||
|
println!("{}: {}", result.id, text);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::process::exit;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
extern crate tokio;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
|
pub async fn run(args: env::Args) {
|
||||||
|
let sauces = utils::get_arg_sauces(args).unwrap();
|
||||||
|
let mut remaining_to_show = sauces.len();
|
||||||
|
if remaining_to_show < 1 {
|
||||||
|
eprintln!("Missing sauce(s)");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut handles: Vec<JoinHandle<String>> = Vec::new();
|
||||||
|
for sauce in sauces {
|
||||||
|
let cloned_client = client.clone();
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
let sauce_info = utils::get_sauce_info(cloned_client, sauce).await.unwrap();
|
||||||
|
let mut text = format!("Sauce: {}\nTitle: ", sauce_info.id);
|
||||||
|
let english_title = sauce_info.title.english;
|
||||||
|
let japanese_title = sauce_info.title.japanese;
|
||||||
|
if english_title != "" {
|
||||||
|
text.push_str(&english_title);
|
||||||
|
if japanese_title != "" {
|
||||||
|
text.push_str(&format!("\nJapanese Title: {}", &japanese_title));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text.push_str(&japanese_title);
|
||||||
|
}
|
||||||
|
let mut tag_hashmap = BTreeMap::new();
|
||||||
|
for tag_info in sauce_info.tags {
|
||||||
|
let tag_key = tag_info.r#type;
|
||||||
|
let tag_value = tag_info.name;
|
||||||
|
let tag_vec = tag_hashmap.entry(tag_key).or_insert(Vec::new());
|
||||||
|
tag_vec.push(tag_value);
|
||||||
|
}
|
||||||
|
for (tag_key, tag_value) in &tag_hashmap {
|
||||||
|
let tag_key = match tag_key.as_str() {
|
||||||
|
"tag" => "Tags",
|
||||||
|
"artist" => "Artists",
|
||||||
|
"parody" => "Parodies",
|
||||||
|
"character" => "Characters",
|
||||||
|
"group" => "Groups",
|
||||||
|
"language" => "Languages",
|
||||||
|
"category" => "Categories",
|
||||||
|
_ => tag_key
|
||||||
|
};
|
||||||
|
text.push_str(&format!("\n{}: {}", tag_key, tag_value.join(", ")));
|
||||||
|
}
|
||||||
|
text.push_str(&format!("\nPages: {}\nFavorites: {}", sauce_info.num_pages, sauce_info.num_favorites));
|
||||||
|
text
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for handle in handles {
|
||||||
|
println!("{}", handle.await.unwrap());
|
||||||
|
if remaining_to_show > 1 {
|
||||||
|
println!("");
|
||||||
|
remaining_to_show -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
mod utils;
|
||||||
|
mod structs;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
commands::run().await
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryTitleInfo {
|
||||||
|
pub english: String,
|
||||||
|
pub japanese: String,
|
||||||
|
pub pretty: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryImageInfo {
|
||||||
|
pub t: String,
|
||||||
|
pub w: usize,
|
||||||
|
pub h: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryImagesInfo {
|
||||||
|
pub pages: Vec<GalleryImageInfo>,
|
||||||
|
pub cover: GalleryImageInfo,
|
||||||
|
pub thumbnail: GalleryImageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryTagInfo {
|
||||||
|
pub id: usize,
|
||||||
|
pub r#type: String,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub count: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct GalleryInfo {
|
||||||
|
pub id: usize,
|
||||||
|
pub media_id: String,
|
||||||
|
pub title: GalleryTitleInfo,
|
||||||
|
pub images: GalleryImagesInfo,
|
||||||
|
pub scanlator: String,
|
||||||
|
pub upload_date: usize,
|
||||||
|
pub tags: Vec<GalleryTagInfo>,
|
||||||
|
pub num_pages: usize,
|
||||||
|
pub num_favorites: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct SearchInfo {
|
||||||
|
pub result: Vec<GalleryInfo>,
|
||||||
|
pub num_pages: usize,
|
||||||
|
pub per_page: usize
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::structs;
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use tokio::stream::StreamExt;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
|
fn fix_gallery(body: &mut serde_json::Value) -> Result<(), serde_json::Error> {
|
||||||
|
if body["id"].is_string() {
|
||||||
|
body["id"] = serde_json::json!(
|
||||||
|
body["id"].as_str().unwrap().parse::<usize>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for title in ["english", "japanese", "pretty"].iter() {
|
||||||
|
if body["title"][title].is_null() {
|
||||||
|
body["title"][title] = serde_json::json!("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_sauce_info(client: reqwest::Client, sauce: usize) -> 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?;
|
||||||
|
assert_eq!(resp.status().is_success(), true);
|
||||||
|
let body = resp.text().await?;
|
||||||
|
let mut body: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||||
|
fix_gallery(&mut body).unwrap();
|
||||||
|
Ok(serde_json::from_str(&serde_json::to_string(&body).unwrap()).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
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?;
|
||||||
|
assert_eq!(resp.status().is_success(), true);
|
||||||
|
let body = resp.text().await?;
|
||||||
|
let mut body: serde_json::Value = serde_json::from_str(&body).unwrap();
|
||||||
|
for i in 0..body["result"].as_array().unwrap().len() {
|
||||||
|
fix_gallery(&mut body["result"][i]).unwrap();
|
||||||
|
}
|
||||||
|
Ok(serde_json::from_str(&serde_json::to_string(&body).unwrap()).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_file(client: reqwest::Client, url: &str, file_name: &str) -> Result<(), reqwest::Error> {
|
||||||
|
let resp = client.get(url)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let mut file = File::create(&file_name).unwrap();
|
||||||
|
let mut stream = resp.bytes_stream();
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
file.write(&item?).unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_arg_sauces(args: env::Args) -> Result<Vec<usize>, String> {
|
||||||
|
let mut sauces: Vec<usize> = Vec::new();
|
||||||
|
for sauce in args {
|
||||||
|
let sauce: usize = match sauce.parse() {
|
||||||
|
Ok(sauce) => sauce,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(format!("{} is not a number/sauce", sauce));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !sauces.contains(&sauce) {
|
||||||
|
sauces.push(sauce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(sauces)
|
||||||
|
}
|
Loading…
Reference in New Issue