hanime changes
This commit is contained in:
@@ -174,99 +174,47 @@ impl HanimeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_video_item(
|
fn db_key(slug: &str) -> String {
|
||||||
&self,
|
format!("https://h.freeanimehentai.net/api/v8/video?id={slug}&")
|
||||||
hit: HanimeSearchResult,
|
}
|
||||||
pool: DbPool,
|
|
||||||
options: ServerOptions,
|
|
||||||
) -> Result<VideoItem> {
|
|
||||||
let mut conn = match pool.get() {
|
|
||||||
Ok(conn) => conn,
|
|
||||||
Err(e) => {
|
|
||||||
report_provider_error("hanime", "get_video_item.pool_get", &e.to_string()).await;
|
|
||||||
return Err(Error::from("Failed to get DB connection"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let db_result = db::get_video(
|
|
||||||
&mut conn,
|
|
||||||
format!(
|
|
||||||
"https://h.freeanimehentai.net/api/v8/video?id={}&",
|
|
||||||
hit.slug.clone()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
drop(conn);
|
|
||||||
let id = hit.id.to_string();
|
|
||||||
let title = hit.name;
|
|
||||||
let thumb = crate::providers::build_proxy_url(
|
|
||||||
&options,
|
|
||||||
"hanime-cdn",
|
|
||||||
&crate::providers::strip_url_scheme(&hit.cover_url),
|
|
||||||
);
|
|
||||||
let duration = (hit.duration_in_ms / 1000) as u32; // Convert ms to seconds
|
|
||||||
let channel = "hanime".to_string(); // Placeholder, adjust as needed
|
|
||||||
match db_result {
|
|
||||||
Ok(Some(video_url)) => {
|
|
||||||
if video_url != "https://streamable.cloud/hls/stream.m3u8" {
|
|
||||||
return Ok(VideoItem::new(
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
video_url.clone(),
|
|
||||||
channel,
|
|
||||||
thumb,
|
|
||||||
duration,
|
|
||||||
)
|
|
||||||
.tags(hit.tags)
|
|
||||||
.uploader(hit.brand)
|
|
||||||
.views(hit.views as u32)
|
|
||||||
.rating((hit.likes as f32 / (hit.likes + hit.dislikes) as f32) * 100 as f32)
|
|
||||||
.aspect_ratio(0.68)
|
|
||||||
.formats(vec![videos::VideoFormat::new(
|
|
||||||
video_url.clone(),
|
|
||||||
"1080".to_string(),
|
|
||||||
"m3u8".to_string(),
|
|
||||||
)]));
|
|
||||||
} else {
|
|
||||||
match pool.get() {
|
|
||||||
Ok(mut conn) => {
|
|
||||||
let _ = db::delete_video(
|
|
||||||
&mut conn,
|
|
||||||
format!(
|
|
||||||
"https://h.freeanimehentai.net/api/v8/video?id={}&",
|
|
||||||
hit.slug.clone()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
report_provider_error_background(
|
|
||||||
"hanime",
|
|
||||||
"get_video_item.delete_video.pool_get",
|
|
||||||
&e.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => (),
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error fetching video from database: {}", e);
|
|
||||||
// return Err(format!("Error fetching video from database: {}", e).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let url = format!(
|
|
||||||
"https://cached.freeanimehentai.net/api/v8/guest/videos/{}/manifest",
|
|
||||||
id
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut requester =
|
fn build_video_item(
|
||||||
crate::providers::requester_or_default(&options, module_path!(), "missing_requester");
|
id: String,
|
||||||
let payload = json!({
|
title: String,
|
||||||
"width": 571, "height": 703, "ab": "kh" }
|
video_url: String,
|
||||||
|
channel: String,
|
||||||
|
thumb: String,
|
||||||
|
duration: u32,
|
||||||
|
tags: Vec<String>,
|
||||||
|
brand: String,
|
||||||
|
views: u64,
|
||||||
|
likes: u64,
|
||||||
|
dislikes: u64,
|
||||||
|
) -> VideoItem {
|
||||||
|
VideoItem::new(id, title, video_url.clone(), channel, thumb, duration)
|
||||||
|
.tags(tags)
|
||||||
|
.uploader(brand)
|
||||||
|
.views(views as u32)
|
||||||
|
.rating((likes as f32 / (likes + dislikes) as f32) * 100_f32)
|
||||||
|
.aspect_ratio(0.68)
|
||||||
|
.formats(vec![videos::VideoFormat::new(
|
||||||
|
video_url,
|
||||||
|
"1080".to_string(),
|
||||||
|
"m3u8".to_string(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_stream_url(&self, id: &str, slug: &str, options: &ServerOptions) -> Result<String> {
|
||||||
|
let manifest_url = format!(
|
||||||
|
"https://cached.freeanimehentai.net/api/v8/guest/videos/{id}/manifest"
|
||||||
);
|
);
|
||||||
|
let mut requester =
|
||||||
|
crate::providers::requester_or_default(options, module_path!(), "missing_requester");
|
||||||
|
let payload = json!({ "width": 571, "height": 703, "ab": "kh" });
|
||||||
let _ = requester
|
let _ = requester
|
||||||
.post_json(
|
.post_json(
|
||||||
&format!(
|
&format!(
|
||||||
"https://cached.freeanimehentai.net/api/v8/hentai_videos/{}/play",
|
"https://cached.freeanimehentai.net/api/v8/hentai_videos/{slug}/play"
|
||||||
hit.slug
|
|
||||||
),
|
),
|
||||||
&payload,
|
&payload,
|
||||||
vec![
|
vec![
|
||||||
@@ -274,11 +222,11 @@ impl HanimeProvider {
|
|||||||
("Referer".to_string(), "https://hanime.tv/".to_string()),
|
("Referer".to_string(), "https://hanime.tv/".to_string()),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await; // Initial request to set cookies
|
.await;
|
||||||
ntex::time::sleep(ntex::time::Seconds(1)).await;
|
ntex::time::sleep(ntex::time::Seconds(1)).await;
|
||||||
let text = requester
|
let text = requester
|
||||||
.get_raw_with_headers(
|
.get_raw_with_headers(
|
||||||
&url,
|
&manifest_url,
|
||||||
vec![
|
vec![
|
||||||
("Origin".to_string(), "https://hanime.tv".to_string()),
|
("Origin".to_string(), "https://hanime.tv".to_string()),
|
||||||
("Referer".to_string(), "https://hanime.tv/".to_string()),
|
("Referer".to_string(), "https://hanime.tv/".to_string()),
|
||||||
@@ -288,77 +236,97 @@ impl HanimeProvider {
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
report_provider_error_background(
|
report_provider_error_background(
|
||||||
"hanime",
|
"hanime",
|
||||||
"get_video_item.get_raw_with_headers",
|
"fetch_stream_url.get_raw_with_headers",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
);
|
);
|
||||||
Error::from(format!("Failed to fetch manifest response: {e}"))
|
Error::from(format!("Failed to fetch manifest: {e}"))
|
||||||
})?
|
})?
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
report_provider_error_background(
|
report_provider_error_background(
|
||||||
"hanime",
|
"hanime",
|
||||||
"get_video_item.response_text",
|
"fetch_stream_url.response_text",
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
);
|
);
|
||||||
Error::from(format!("Failed to decode manifest response body: {e}"))
|
Error::from(format!("Failed to decode manifest body: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if text.contains("Unautho") {
|
if text.contains("Unautho") {
|
||||||
println!("Fetched video details for {}: {}", title, text);
|
|
||||||
return Err(Error::from("Unauthorized"));
|
return Err(Error::from("Unauthorized"));
|
||||||
}
|
}
|
||||||
let urls = text
|
|
||||||
|
let urls_section = text
|
||||||
.split("streams")
|
.split("streams")
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.ok_or_else(|| Error::from("Missing streams section in manifest"))?;
|
.ok_or_else(|| Error::from("Missing streams section in manifest"))?;
|
||||||
let mut url_vec = vec![];
|
|
||||||
|
|
||||||
for el in urls.split("\"url\":\"").collect::<Vec<&str>>() {
|
let mut url_vec = vec![];
|
||||||
let url = el
|
for el in urls_section.split("\"url\":\"") {
|
||||||
.split("\"")
|
let url = el.split('"').next().unwrap_or_default();
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.get(0)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or_default();
|
|
||||||
if !url.is_empty() && url.contains("m3u8") {
|
if !url.is_empty() && url.contains("m3u8") {
|
||||||
url_vec.push(url.to_string());
|
url_vec.push(url.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let first_url = url_vec
|
|
||||||
.first()
|
url_vec
|
||||||
.cloned()
|
.into_iter()
|
||||||
.ok_or_else(|| Error::from("No stream URL found in manifest"))?;
|
.next()
|
||||||
match pool.get() {
|
.ok_or_else(|| Error::from("No stream URL found in manifest"))
|
||||||
Ok(mut conn) => {
|
}
|
||||||
let _ = db::insert_video(
|
|
||||||
&mut conn,
|
async fn get_video_item(
|
||||||
&format!(
|
&self,
|
||||||
"https://h.freeanimehentai.net/api/v8/video?id={}&",
|
hit: HanimeSearchResult,
|
||||||
hit.slug.clone()
|
pool: DbPool,
|
||||||
),
|
options: ServerOptions,
|
||||||
&first_url,
|
) -> Result<VideoItem> {
|
||||||
);
|
let id = hit.id.to_string();
|
||||||
|
let title = hit.name;
|
||||||
|
let thumb = crate::providers::build_proxy_url(
|
||||||
|
&options,
|
||||||
|
"hanime-cdn",
|
||||||
|
&crate::providers::strip_url_scheme(&hit.cover_url),
|
||||||
|
);
|
||||||
|
let duration = (hit.duration_in_ms / 1000) as u32;
|
||||||
|
let channel = "hanime".to_string();
|
||||||
|
let db_key = Self::db_key(&hit.slug);
|
||||||
|
|
||||||
|
match self.fetch_stream_url(&id, &hit.slug, &options).await {
|
||||||
|
Ok(stream_url) => {
|
||||||
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
let _ = db::insert_video(&mut conn, &db_key, &stream_url);
|
||||||
|
}
|
||||||
|
return Ok(Self::build_video_item(
|
||||||
|
id, title, stream_url, channel, thumb, duration,
|
||||||
|
hit.tags, hit.brand, hit.views, hit.likes, hit.dislikes,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
report_provider_error_background(
|
report_provider_error_background("hanime", "get_video_item.fetch_stream_url", &e.to_string());
|
||||||
"hanime",
|
|
||||||
"get_video_item.insert_video.pool_get",
|
|
||||||
&e.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(
|
|
||||||
VideoItem::new(id, title, first_url.clone(), channel, thumb, duration)
|
// API failed — fall back to DB
|
||||||
.tags(hit.tags)
|
let db_result = pool.get().ok().and_then(|mut conn| {
|
||||||
.uploader(hit.brand)
|
db::get_video(&mut conn, db_key.clone()).ok().flatten()
|
||||||
.views(hit.views as u32)
|
});
|
||||||
.rating((hit.likes as f32 / (hit.likes + hit.dislikes) as f32) * 100 as f32)
|
|
||||||
.formats(vec![videos::VideoFormat::new(
|
match db_result {
|
||||||
first_url,
|
Some(video_url) if video_url != "https://streamable.cloud/hls/stream.m3u8" => {
|
||||||
"1080".to_string(),
|
Ok(Self::build_video_item(
|
||||||
"m3u8".to_string(),
|
id, title, video_url, channel, thumb, duration,
|
||||||
)]),
|
hit.tags, hit.brand, hit.views, hit.likes, hit.dislikes,
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
if let Ok(mut conn) = pool.get() {
|
||||||
|
let _ = db::delete_video(&mut conn, db_key);
|
||||||
|
}
|
||||||
|
Err(Error::from("Stale DB entry and API unavailable"))
|
||||||
|
}
|
||||||
|
None => Err(Error::from("API unavailable and no DB fallback")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(
|
async fn get(
|
||||||
|
|||||||
112
src/proxies/hanimethumb.rs
Normal file
112
src/proxies/hanimethumb.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use ntex::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||||
|
use ntex::{
|
||||||
|
http::Response,
|
||||||
|
web::{self, HttpRequest, error},
|
||||||
|
};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
use crate::util::requester::Requester;
|
||||||
|
|
||||||
|
fn normalize_page_url(endpoint: &str) -> String {
|
||||||
|
let endpoint = endpoint.trim_start_matches('/');
|
||||||
|
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
|
||||||
|
endpoint.to_string()
|
||||||
|
} else if endpoint.starts_with("hanime.tv/") {
|
||||||
|
format!("https://{endpoint}")
|
||||||
|
} else {
|
||||||
|
format!("https://hanime.tv/videos/hentai/{endpoint}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_cover_url(page_url: &str, requester: &Requester) -> Option<String> {
|
||||||
|
let html = requester
|
||||||
|
.clone()
|
||||||
|
.get_raw_with_headers(
|
||||||
|
page_url,
|
||||||
|
vec![("Referer".to_string(), "https://hanime.tv/".to_string())],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let doc = Html::parse_document(&html);
|
||||||
|
let selector = Selector::parse("div.hvpi-cover-container img.hvpi-cover").ok()?;
|
||||||
|
let img = doc.select(&selector).next()?;
|
||||||
|
img.value().attr("src").map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_image(
|
||||||
|
req: HttpRequest,
|
||||||
|
requester: web::types::State<Requester>,
|
||||||
|
) -> Result<impl web::Responder, web::Error> {
|
||||||
|
let endpoint = req.match_info().query("endpoint").to_string();
|
||||||
|
let page_url = normalize_page_url(&endpoint);
|
||||||
|
|
||||||
|
let cover_url = match fetch_cover_url(&page_url, requester.get_ref()).await {
|
||||||
|
Some(url) => url,
|
||||||
|
None => return Ok(web::HttpResponse::NotFound().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let upstream = match requester
|
||||||
|
.get_ref()
|
||||||
|
.clone()
|
||||||
|
.get_raw_with_headers(
|
||||||
|
&cover_url,
|
||||||
|
vec![("Referer".to_string(), "https://hanime.tv/".to_string())],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(_) => return Ok(web::HttpResponse::NotFound().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = upstream.status();
|
||||||
|
let headers = upstream.headers().clone();
|
||||||
|
let bytes = upstream.bytes().await.map_err(error::ErrorBadGateway)?;
|
||||||
|
|
||||||
|
let mut resp = Response::build(status);
|
||||||
|
|
||||||
|
if let Some(ct) = headers.get(CONTENT_TYPE) {
|
||||||
|
if let Ok(ct_str) = ct.to_str() {
|
||||||
|
resp.set_header(CONTENT_TYPE, ct_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(cl) = headers.get(CONTENT_LENGTH) {
|
||||||
|
if let Ok(cl_str) = cl.to_str() {
|
||||||
|
resp.set_header(CONTENT_LENGTH, cl_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(resp.body(bytes.to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::normalize_page_url;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slug_becomes_full_url() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_page_url("reika-wa-karei-na-boku-no-joou-3"),
|
||||||
|
"https://hanime.tv/videos/hentai/reika-wa-karei-na-boku-no-joou-3"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_url_passes_through() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_page_url("https://hanime.tv/videos/hentai/reika-wa-karei-na-boku-no-joou-3"),
|
||||||
|
"https://hanime.tv/videos/hentai/reika-wa-karei-na-boku-no-joou-3"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hanime_tv_host_gets_scheme() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_page_url("hanime.tv/videos/hentai/some-slug"),
|
||||||
|
"https://hanime.tv/videos/hentai/some-slug"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub mod archivebate;
|
|||||||
pub mod clapdat;
|
pub mod clapdat;
|
||||||
pub mod doodstream;
|
pub mod doodstream;
|
||||||
pub mod hanimecdn;
|
pub mod hanimecdn;
|
||||||
|
pub mod hanimethumb;
|
||||||
pub mod heavyfetish;
|
pub mod heavyfetish;
|
||||||
pub mod hqporner;
|
pub mod hqporner;
|
||||||
pub mod hqpornerthumb;
|
pub mod hqpornerthumb;
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.route(web::post().to(crate::proxies::hanimecdn::get_image))
|
.route(web::post().to(crate::proxies::hanimecdn::get_image))
|
||||||
.route(web::get().to(crate::proxies::hanimecdn::get_image)),
|
.route(web::get().to(crate::proxies::hanimecdn::get_image)),
|
||||||
)
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/hanime-thumb/{endpoint}*")
|
||||||
|
.route(web::post().to(crate::proxies::hanimethumb::get_image))
|
||||||
|
.route(web::get().to(crate::proxies::hanimethumb::get_image)),
|
||||||
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/hqporner-thumb/{endpoint}*")
|
web::resource("/hqporner-thumb/{endpoint}*")
|
||||||
.route(web::post().to(crate::proxies::hqpornerthumb::get_image))
|
.route(web::post().to(crate::proxies::hqpornerthumb::get_image))
|
||||||
|
|||||||
Reference in New Issue
Block a user