diff --git a/src/providers/hanime.rs b/src/providers/hanime.rs index fe41791..f43ae6c 100644 --- a/src/providers/hanime.rs +++ b/src/providers/hanime.rs @@ -174,99 +174,47 @@ impl HanimeProvider { } } - async fn get_video_item( - &self, - hit: HanimeSearchResult, - pool: DbPool, - options: ServerOptions, - ) -> Result { - 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 - ); + fn db_key(slug: &str) -> String { + format!("https://h.freeanimehentai.net/api/v8/video?id={slug}&") + } - let mut requester = - crate::providers::requester_or_default(&options, module_path!(), "missing_requester"); - let payload = json!({ - "width": 571, "height": 703, "ab": "kh" } + fn build_video_item( + id: String, + title: String, + video_url: String, + channel: String, + thumb: String, + duration: u32, + tags: Vec, + 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 { + 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 .post_json( &format!( - "https://cached.freeanimehentai.net/api/v8/hentai_videos/{}/play", - hit.slug + "https://cached.freeanimehentai.net/api/v8/hentai_videos/{slug}/play" ), &payload, vec![ @@ -274,11 +222,11 @@ impl HanimeProvider { ("Referer".to_string(), "https://hanime.tv/".to_string()), ], ) - .await; // Initial request to set cookies + .await; ntex::time::sleep(ntex::time::Seconds(1)).await; let text = requester .get_raw_with_headers( - &url, + &manifest_url, vec![ ("Origin".to_string(), "https://hanime.tv".to_string()), ("Referer".to_string(), "https://hanime.tv/".to_string()), @@ -288,77 +236,97 @@ impl HanimeProvider { .map_err(|e| { report_provider_error_background( "hanime", - "get_video_item.get_raw_with_headers", + "fetch_stream_url.get_raw_with_headers", &e.to_string(), ); - Error::from(format!("Failed to fetch manifest response: {e}")) + Error::from(format!("Failed to fetch manifest: {e}")) })? .text() .await .map_err(|e| { report_provider_error_background( "hanime", - "get_video_item.response_text", + "fetch_stream_url.response_text", &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") { - println!("Fetched video details for {}: {}", title, text); return Err(Error::from("Unauthorized")); } - let urls = text + + let urls_section = text .split("streams") .nth(1) .ok_or_else(|| Error::from("Missing streams section in manifest"))?; - let mut url_vec = vec![]; - for el in urls.split("\"url\":\"").collect::>() { - let url = el - .split("\"") - .collect::>() - .get(0) - .copied() - .unwrap_or_default(); + let mut url_vec = vec![]; + for el in urls_section.split("\"url\":\"") { + let url = el.split('"').next().unwrap_or_default(); if !url.is_empty() && url.contains("m3u8") { url_vec.push(url.to_string()); } } - let first_url = url_vec - .first() - .cloned() - .ok_or_else(|| Error::from("No stream URL found in manifest"))?; - match pool.get() { - Ok(mut conn) => { - let _ = db::insert_video( - &mut conn, - &format!( - "https://h.freeanimehentai.net/api/v8/video?id={}&", - hit.slug.clone() - ), - &first_url, - ); + + url_vec + .into_iter() + .next() + .ok_or_else(|| Error::from("No stream URL found in manifest")) + } + + async fn get_video_item( + &self, + hit: HanimeSearchResult, + pool: DbPool, + options: ServerOptions, + ) -> Result { + 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) => { - report_provider_error_background( - "hanime", - "get_video_item.insert_video.pool_get", - &e.to_string(), - ); + report_provider_error_background("hanime", "get_video_item.fetch_stream_url", &e.to_string()); } } - Ok( - VideoItem::new(id, title, first_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) - .formats(vec![videos::VideoFormat::new( - first_url, - "1080".to_string(), - "m3u8".to_string(), - )]), - ) + + // API failed — fall back to DB + let db_result = pool.get().ok().and_then(|mut conn| { + db::get_video(&mut conn, db_key.clone()).ok().flatten() + }); + + match db_result { + Some(video_url) if video_url != "https://streamable.cloud/hls/stream.m3u8" => { + Ok(Self::build_video_item( + 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( diff --git a/src/proxies/hanimethumb.rs b/src/proxies/hanimethumb.rs new file mode 100644 index 0000000..ab74e0c --- /dev/null +++ b/src/proxies/hanimethumb.rs @@ -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 { + 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, +) -> Result { + 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" + ); + } +} diff --git a/src/proxies/mod.rs b/src/proxies/mod.rs index 394f177..4f7be05 100644 --- a/src/proxies/mod.rs +++ b/src/proxies/mod.rs @@ -23,6 +23,7 @@ pub mod archivebate; pub mod clapdat; pub mod doodstream; pub mod hanimecdn; +pub mod hanimethumb; pub mod heavyfetish; pub mod hqporner; pub mod hqpornerthumb; diff --git a/src/proxy.rs b/src/proxy.rs index 92c0885..02af129 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -112,6 +112,11 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route(web::post().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( web::resource("/hqporner-thumb/{endpoint}*") .route(web::post().to(crate::proxies::hqpornerthumb::get_image))