Initial commit
This commit is contained in:
commit
e03d746ce6
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "hentaihavenrs"
|
||||
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]
|
||||
tokio = { version = "1.1", features = ["rt"] }
|
||||
reqwest = { version = "0.11", features = ["multipart", "rustls-tls"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
quick-xml = "0.20"
|
||||
clap = { version = "2.33", default-features = false }
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 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,104 @@
|
|||
use crate::utils;
|
||||
|
||||
use std::io;
|
||||
use std::fs::{rename, create_dir};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{exit, Command};
|
||||
use clap::ArgMatches;
|
||||
|
||||
const MAX_DOWNLOAD_ATTEMPTS: i32 = 5;
|
||||
|
||||
pub async fn download(arg_m: &ArgMatches<'_>) {
|
||||
let print_only = arg_m.is_present("print");
|
||||
let id = arg_m.value_of("id").unwrap();
|
||||
let client = utils::create_client();
|
||||
let mut return_fail = false;
|
||||
let hentai_info = utils::get_hentai(client.clone(), id).await;
|
||||
match hentai_info {
|
||||
Ok(hentai_info) => {
|
||||
match hentai_info {
|
||||
Some(hentai_info) => {
|
||||
let tcloned_client = client.clone();
|
||||
let iter = (1..=hentai_info.episode_urls.len()).zip(hentai_info.episode_urls);
|
||||
for (episode_number, episode_url) in iter {
|
||||
let mut filename = PathBuf::from(&hentai_info.slug);
|
||||
filename.push(&format!("{}.mkv", episode_number));
|
||||
if filename.exists() {
|
||||
continue;
|
||||
}
|
||||
let download_url = match utils::get_url(tcloned_client.clone(), &episode_url).await {
|
||||
Ok(Some(i)) => i,
|
||||
Ok(None) => {
|
||||
eprintln!("Failed to get {}: get_url returned None", filename.display());
|
||||
continue;
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Failed to get {}: {}", filename.display(), err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if print_only {
|
||||
println!("{}", download_url);
|
||||
continue;
|
||||
}
|
||||
match create_dir(&hentai_info.slug) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
if err.kind() != io::ErrorKind::AlreadyExists {
|
||||
eprintln!("Failed to create parent directory due to {}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut tmp_filename = filename.clone();
|
||||
tmp_filename.set_extension("tmp");
|
||||
let mut fail_dl = true;
|
||||
for i in 0..MAX_DOWNLOAD_ATTEMPTS {
|
||||
eprintln!("Downloading {} (attempt {})", filename.display(), i);
|
||||
let mut command = Command::new("ffmpeg");
|
||||
let command = command.args(&["-v", "warning", "-stats", "-nostdin", "-y", "-i"]);
|
||||
let mut command = command.arg(&download_url.video);
|
||||
if let Some(ref captions) = download_url.captions {
|
||||
command = command.args(&["-i", &captions]);
|
||||
}
|
||||
match command.args(&["-c", "copy", "-f", "matroska"]).arg(&tmp_filename).spawn() {
|
||||
Ok(mut child) => {
|
||||
match child.wait() {
|
||||
Ok(exit_status) => {
|
||||
if exit_status.success() {
|
||||
fail_dl = false;
|
||||
match rename(&tmp_filename, &filename) {
|
||||
Ok(_) => (),
|
||||
Err(err) => eprintln!("Failed to rename {} to {} due to {}", tmp_filename.display(), filename.display(), err)
|
||||
};
|
||||
break;
|
||||
}
|
||||
eprintln!("ffmpeg exited with {:?}", exit_status.code());
|
||||
},
|
||||
Err(err) => eprintln!("Failed to wait on ffmpeg process due to {}", err)
|
||||
};
|
||||
},
|
||||
Err(err) => eprintln!("Failed to spawn ffmpeg process due to {}", err)
|
||||
};
|
||||
}
|
||||
if fail_dl {
|
||||
eprintln!("Failed to download {}", filename.display());
|
||||
return_fail = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
eprintln!("Failed to get {}: does not exist", id);
|
||||
return_fail = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Failed to get {}: {}", id, err);
|
||||
return_fail = true;
|
||||
}
|
||||
};
|
||||
if return_fail {
|
||||
exit(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
mod view;
|
||||
mod search;
|
||||
mod download;
|
||||
pub use view::view;
|
||||
pub use search::search;
|
||||
pub use download::download;
|
|
@ -0,0 +1,17 @@
|
|||
use crate::utils;
|
||||
|
||||
use std::process::exit;
|
||||
use clap::ArgMatches;
|
||||
|
||||
pub async fn search(arg_m: &ArgMatches<'_>) {
|
||||
let query = arg_m.values_of("query").unwrap_or_default().collect::<Vec<_>>().join(" ");
|
||||
let query = query.trim();
|
||||
let results = utils::search(utils::create_client(), query).await.unwrap();
|
||||
if results.is_empty() {
|
||||
eprintln!("No results found");
|
||||
exit(1);
|
||||
}
|
||||
for i in results {
|
||||
println!("{}", i);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
use crate::utils;
|
||||
|
||||
use std::process::exit;
|
||||
use clap::ArgMatches;
|
||||
extern crate tokio;
|
||||
|
||||
pub async fn view(arg_m: &ArgMatches<'_>) {
|
||||
let client = utils::create_client();
|
||||
let handles = arg_m.values_of("id").unwrap().map(|id| {
|
||||
let cloned_client = client.clone();
|
||||
let id = id.to_string();
|
||||
let cid = id.clone();
|
||||
(tokio::spawn(async move {
|
||||
utils::get_hentai(cloned_client, &cid).await
|
||||
}), id)
|
||||
}).collect::<Vec<_>>();
|
||||
let mut fail = false;
|
||||
let mut one_done = false;
|
||||
for handle in handles {
|
||||
let (handle, id) = handle;
|
||||
let hentai = match handle.await {
|
||||
Ok(hentai) => hentai,
|
||||
Err(err) => {
|
||||
if one_done {
|
||||
eprintln!("");
|
||||
}
|
||||
eprintln!("ID: {}\nError: {}", id, err);
|
||||
fail = true;
|
||||
one_done = true;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match hentai {
|
||||
Ok(hentai) => {
|
||||
match hentai {
|
||||
Some(hentai) => {
|
||||
if one_done {
|
||||
println!("");
|
||||
}
|
||||
println!("ID: {}\n{}", id, &hentai);
|
||||
},
|
||||
None => {
|
||||
if one_done {
|
||||
eprintln!("");
|
||||
}
|
||||
eprintln!("ID: {}\nError: does not exist", id);
|
||||
fail = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
Err(err) => {
|
||||
if one_done {
|
||||
eprintln!("");
|
||||
}
|
||||
eprintln!("ID: {}\nError: {}", id, err);
|
||||
fail = true;
|
||||
}
|
||||
};
|
||||
one_done = true;
|
||||
}
|
||||
if fail {
|
||||
exit(1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
mod commands;
|
||||
mod structs;
|
||||
mod utils;
|
||||
use clap::{App, AppSettings, Arg, SubCommand};
|
||||
extern crate tokio;
|
||||
|
||||
fn main() {
|
||||
let matches = App::new("hentaihavenrs")
|
||||
.about("hentaihaven.tv downloader in rust")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(
|
||||
SubCommand::with_name("search")
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.help("Search query")
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("view")
|
||||
.aliases(&["info", "show"])
|
||||
.arg(
|
||||
Arg::with_name("id")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("download")
|
||||
.alias("dl")
|
||||
.arg(
|
||||
Arg::with_name("print")
|
||||
.long("print")
|
||||
.short("p")
|
||||
.help("Print the URL to download only")
|
||||
).arg(
|
||||
Arg::with_name("id")
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
)
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
match matches.subcommand() {
|
||||
("search", Some(sub_m)) => runtime.block_on(commands::search(sub_m)),
|
||||
("view", Some(sub_m)) => runtime.block_on(commands::view(sub_m)),
|
||||
("download", Some(sub_m)) => runtime.block_on(commands::download(sub_m)),
|
||||
_ => panic!("AppSettings::SubcommandRequiredElseHelp do your job please")
|
||||
};
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
use std::fmt;
|
||||
use serde::Deserialize;
|
||||
extern crate reqwest;
|
||||
extern crate quick_xml;
|
||||
extern crate serde_json;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct SearchResult {
|
||||
pub id: i32,
|
||||
pub title: RenderedTitle
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct RenderedTitle {
|
||||
pub rendered: String
|
||||
}
|
||||
|
||||
impl fmt::Display for SearchResult {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&format!("{}: {}", self.id, &self.title.rendered))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HentaiInfo {
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
pub views: usize,
|
||||
pub genres: Vec<String>,
|
||||
pub censored: bool,
|
||||
pub episode_urls: Vec<String>,
|
||||
pub summary: String
|
||||
}
|
||||
|
||||
impl fmt::Display for HentaiInfo {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "Title: {}\nViews: {}\nCensored: {}\nGenres: {}\nEpisodes: {}\nSummary:\n{}",
|
||||
&self.title,
|
||||
self.views,
|
||||
match self.censored {
|
||||
true => "Yes",
|
||||
false => "No"
|
||||
},
|
||||
&self.genres.join(", "),
|
||||
self.episode_urls.len(),
|
||||
&self.summary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HentaiVideo {
|
||||
pub captions: Option<String>,
|
||||
pub video: String
|
||||
}
|
||||
|
||||
impl fmt::Display for HentaiVideo {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut text = self.video.clone();
|
||||
if let Some(subtitles) = &self.captions {
|
||||
text.push(';');
|
||||
text.push_str(&subtitles);
|
||||
}
|
||||
formatter.write_str(&text)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct RawHentaiVideo {
|
||||
pub data: RawHentaiVideoData
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct RawHentaiVideoData {
|
||||
pub captions: RawHentaiVideoSrc,
|
||||
pub sources: Vec<RawHentaiVideoSrc>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct RawHentaiVideoSrc {
|
||||
pub src: String
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Reqwest(reqwest::Error),
|
||||
QuickXML(quick_xml::Error),
|
||||
SerdeJSON(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
#[inline]
|
||||
fn from(error: reqwest::Error) -> Error {
|
||||
Error::Reqwest(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<quick_xml::Error> for Error {
|
||||
#[inline]
|
||||
fn from(error: quick_xml::Error) -> Error {
|
||||
Error::QuickXML(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
#[inline]
|
||||
fn from(error: serde_json::Error) -> Error {
|
||||
Error::SerdeJSON(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(
|
||||
&match self {
|
||||
Error::Reqwest(err) => format!("reqwest error: {}", err),
|
||||
Error::QuickXML(err) => format!("quick-xml error: {}", err),
|
||||
Error::SerdeJSON(err) => format!("serde_json error: {}", err),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
use crate::structs;
|
||||
|
||||
use quick_xml::Reader;
|
||||
use quick_xml::events::Event;
|
||||
extern crate reqwest;
|
||||
extern crate serde_json;
|
||||
|
||||
pub async fn search(client: reqwest::Client, query: &str) -> Result<Vec<structs::SearchResult>, structs::Error> {
|
||||
let text = &client.get("https://hentaihaven.xxx/wp-json/wp/v2/wp-manga")
|
||||
.query(&[("search", &query)])
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let text = text.trim_start_matches("\u{feff}");
|
||||
Ok(serde_json::from_str(&text)?)
|
||||
}
|
||||
|
||||
pub async fn get_hentai(client: reqwest::Client, id: &str) -> Result<Option<structs::HentaiInfo>, structs::Error> {
|
||||
let url = match id.contains(|c: char| !c.is_digit(10)) {
|
||||
true => format!("https://hentaihaven.xxx/watch/{}", &id),
|
||||
false => format!("https://hentaihaven.xxx/?post_type=wp-manga&p={}", &id)
|
||||
};
|
||||
let resp = client.get(&url)
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
return Ok(None);
|
||||
}
|
||||
let slug = resp.url().path().trim_end_matches('/').rsplitn(2, '/').nth(0).unwrap().to_string();
|
||||
let text = resp.text().await?.replace(" ", " ");
|
||||
let mut reader = Reader::from_str(&text);
|
||||
reader.check_end_names(false);
|
||||
let mut buf = Vec::new();
|
||||
let mut is_inside_a = false;
|
||||
let mut is_inside_summary = false;
|
||||
let mut is_inside_nav_links = false;
|
||||
let mut is_inside_post_title = false;
|
||||
let mut is_inside_chapter_list = false;
|
||||
let mut is_inside_summary_heading = false;
|
||||
let mut is_inside_summary_content = false;
|
||||
let mut to_read_rank = false;
|
||||
let mut to_read_genres = false;
|
||||
let mut rank = 0;
|
||||
let mut title = String::new();
|
||||
let mut genres = Vec::new();
|
||||
let mut censored = true;
|
||||
let mut episode_urls = Vec::new();
|
||||
let mut summary = String::new();
|
||||
loop {
|
||||
match reader.read_event(&mut buf) {
|
||||
Ok(Event::Start(ref e)) => {
|
||||
if e.name() == b"div" {
|
||||
let class = e.attributes()
|
||||
.find(|i| {
|
||||
match i.as_ref() {
|
||||
Ok(i) => i.key == b"class",
|
||||
Err(_) => false
|
||||
}
|
||||
});
|
||||
if let Some(class) = class {
|
||||
match class.unwrap().unescape_and_decode_value(&reader) {
|
||||
Ok(class_name) => {
|
||||
match class_name.as_str() {
|
||||
"summary-heading" => is_inside_summary_heading = true,
|
||||
"summary-content" => is_inside_summary_content = true,
|
||||
"post-title" => is_inside_post_title = true,
|
||||
"nav-links" => is_inside_nav_links = true,
|
||||
"listing-chapters_wrap" => is_inside_chapter_list = true,
|
||||
"summary__content show-more" => is_inside_summary = true,
|
||||
_ => ()
|
||||
};
|
||||
},
|
||||
Err(_) => ()
|
||||
};
|
||||
}
|
||||
} else if e.name() == b"a" {
|
||||
is_inside_a = true;
|
||||
if is_inside_nav_links {
|
||||
let class = e.attributes()
|
||||
.find(|i| {
|
||||
match i.as_ref() {
|
||||
Ok(i) => i.key == b"class",
|
||||
Err(_) => false
|
||||
}
|
||||
});
|
||||
if let Some(class) = class {
|
||||
match class.unwrap().unescape_and_decode_value(&reader) {
|
||||
Ok(class_name) => {
|
||||
if class_name.to_lowercase().split_whitespace().any(|i| i == "uncensored") {
|
||||
censored = false;
|
||||
is_inside_nav_links = false;
|
||||
}
|
||||
},
|
||||
Err(_) => ()
|
||||
};
|
||||
}
|
||||
} else if is_inside_chapter_list {
|
||||
let href = e.attributes()
|
||||
.find(|i| {
|
||||
match i.as_ref() {
|
||||
Ok(i) => i.key == b"href",
|
||||
Err(_) => false
|
||||
}
|
||||
});
|
||||
if let Some(href) = href {
|
||||
match href.unwrap().unescape_and_decode_value(&reader) {
|
||||
Ok(href) => episode_urls.push(href),
|
||||
Err(_) => ()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = match e.unescape_and_decode(&reader) {
|
||||
Ok(text) => text,
|
||||
Err(_) => continue
|
||||
};
|
||||
if is_inside_summary_heading {
|
||||
match text.trim() {
|
||||
"Rank" => to_read_rank = true,
|
||||
"Genre(s)" => to_read_genres = true,
|
||||
_ => ()
|
||||
};
|
||||
} else if is_inside_summary_content {
|
||||
if to_read_rank {
|
||||
match text.trim().splitn(2, " ").nth(0).unwrap().parse::<usize>() {
|
||||
Ok(i) => rank = i,
|
||||
Err(_) => ()
|
||||
};
|
||||
to_read_rank = false;
|
||||
} else if to_read_genres && is_inside_a {
|
||||
genres.push(text.to_string());
|
||||
}
|
||||
} else if is_inside_post_title {
|
||||
title.push_str(&text);
|
||||
} else if is_inside_summary {
|
||||
summary.push_str(&text);
|
||||
}
|
||||
},
|
||||
Ok(Event::End(ref e)) => {
|
||||
if e.name() == b"div" {
|
||||
if is_inside_summary_heading {
|
||||
is_inside_summary_heading = false;
|
||||
} else if is_inside_summary_content {
|
||||
is_inside_summary_content = false;
|
||||
to_read_genres = false;
|
||||
} else if is_inside_post_title {
|
||||
is_inside_post_title = false;
|
||||
title = title.trim().to_string();
|
||||
} else if is_inside_nav_links {
|
||||
is_inside_nav_links = false;
|
||||
} else if is_inside_chapter_list {
|
||||
is_inside_chapter_list = false;
|
||||
} else if is_inside_summary {
|
||||
break;
|
||||
}
|
||||
} else if e.name() == b"a" {
|
||||
is_inside_a = false;
|
||||
}
|
||||
},
|
||||
Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err),
|
||||
Ok(Event::Eof) => break,
|
||||
_ => ()
|
||||
};
|
||||
buf.clear();
|
||||
}
|
||||
episode_urls.reverse();
|
||||
summary = summary.trim().to_string();
|
||||
Ok(Some(structs::HentaiInfo {
|
||||
slug: slug,
|
||||
title: title,
|
||||
views: rank,
|
||||
genres: genres,
|
||||
censored: censored,
|
||||
episode_urls: episode_urls,
|
||||
summary: summary
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_url(client: reqwest::Client, url: &str) -> Result<Option<structs::HentaiVideo>, structs::Error> {
|
||||
let resp = client.get(url)
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = resp.text().await?;
|
||||
let mut reader = Reader::from_str(&text);
|
||||
reader.check_end_names(false);
|
||||
let mut buf = Vec::new();
|
||||
let mut iframe_url = None;
|
||||
loop {
|
||||
match reader.read_event(&mut buf) {
|
||||
Ok(Event::Start(ref e)) if e.name() == b"iframe" => {
|
||||
let src = e.attributes()
|
||||
.find(|i| {
|
||||
match i.as_ref() {
|
||||
Ok(i) => i.key == b"src",
|
||||
Err(_) => false
|
||||
}
|
||||
});
|
||||
if let Some(src) = src {
|
||||
match src.unwrap().unescape_and_decode_value(&reader) {
|
||||
Ok(src) => {
|
||||
iframe_url = Some(src);
|
||||
break
|
||||
},
|
||||
Err(_) => ()
|
||||
};
|
||||
}
|
||||
},
|
||||
Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err),
|
||||
Ok(Event::Eof) => break,
|
||||
_ => ()
|
||||
};
|
||||
buf.clear();
|
||||
}
|
||||
let iframe_url = match iframe_url {
|
||||
Some(tmp) => tmp,
|
||||
None => return Ok(None)
|
||||
};
|
||||
parse_iframe(client, &iframe_url).await
|
||||
}
|
||||
|
||||
async fn parse_iframe(client: reqwest::Client, url: &str) -> Result<Option<structs::HentaiVideo>, structs::Error> {
|
||||
let resp = client.get(url)
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = resp.text().await?;
|
||||
let mut reader = Reader::from_str(&text);
|
||||
reader.check_end_names(false);
|
||||
let mut buf = Vec::new();
|
||||
let mut form = reqwest::multipart::Form::new();
|
||||
let mut form_modified = false;
|
||||
loop {
|
||||
match reader.read_event(&mut buf) {
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = match reader.decode(e.escaped()) {
|
||||
Ok(text) => text,
|
||||
Err(_) => continue
|
||||
};
|
||||
for i in text.split('\n') {
|
||||
let i = i.trim();
|
||||
if !i.starts_with("data.append('") {
|
||||
continue;
|
||||
}
|
||||
let mut i = i.trim_start_matches("data.append('").trim_end_matches("');").splitn(2, "', '");
|
||||
let key = match i.next() {
|
||||
Some(i) => i,
|
||||
None => continue
|
||||
};
|
||||
let value = match i.next() {
|
||||
Some(i) => i,
|
||||
None => continue
|
||||
};
|
||||
form = form.text(key.to_string(), value.to_string());
|
||||
form_modified = true;
|
||||
}
|
||||
},
|
||||
Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err),
|
||||
Ok(Event::Eof) => break,
|
||||
_ => ()
|
||||
};
|
||||
buf.clear();
|
||||
}
|
||||
if !form_modified {
|
||||
return Ok(None);
|
||||
}
|
||||
let text = client.post("https://hentaihaven.xxx/wp-admin/admin-ajax.php")
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
let text = text.trim_start_matches("\u{feff}");
|
||||
let raw_data: structs::RawHentaiVideo = serde_json::from_str(&text)?;
|
||||
let raw_data = raw_data.data;
|
||||
let captions = match client.get(&raw_data.captions.src).send().await?.status().as_u16() {
|
||||
200 => Some(raw_data.captions.src),
|
||||
_ => None
|
||||
};
|
||||
let video_url = match raw_data.sources.get(0) {
|
||||
Some(i) => i.src.clone(),
|
||||
None => return Ok(None)
|
||||
};
|
||||
Ok(Some(structs::HentaiVideo {
|
||||
captions: captions,
|
||||
video: video_url
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn create_client() -> reqwest::Client {
|
||||
// cloudflare you can go fuck yourself
|
||||
reqwest::ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.http1_title_case_headers()
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0")
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
Loading…
Reference in New Issue