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