use async_trait::async_trait; use error_chain::error_chain; use futures::future::join_all; use serde_json::json; use std::vec; use crate::DbPool; use crate::api::ClientVersion; use crate::db; use crate::providers::{Provider, report_provider_error, report_provider_error_background}; use crate::status::*; use crate::util::cache::VideoCache; use crate::videos::{self, ServerOptions, VideoItem}; pub const CHANNEL_METADATA: crate::providers::ProviderChannelMetadata = crate::providers::ProviderChannelMetadata { group_id: "hentai-animation", tags: &["hentai", "anime", "premium"], }; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct HanimeSearchRequest { search_text: String, tags: Vec, tags_mode: String, brands: Vec, blacklist: Vec, order_by: String, ordering: String, page: u8, } impl HanimeSearchRequest { pub fn new() -> Self { HanimeSearchRequest { search_text: "".to_string(), tags: vec![], tags_mode: "AND".to_string(), brands: vec![], blacklist: vec![], order_by: "created_at_unix".to_string(), ordering: "desc".to_string(), page: 0, } } pub fn search_text(mut self, search_text: String) -> Self { self.search_text = search_text; self } pub fn order_by(mut self, order_by: String) -> Self { self.order_by = order_by; self } pub fn ordering(mut self, ordering: String) -> Self { self.ordering = ordering; self } pub fn page(mut self, page: u8) -> Self { self.page = page; self } } #[derive(serde::Serialize, serde::Deserialize, Debug)] struct HanimeSearchResponse { page: u8, nbPages: u8, nbHits: u32, hitsPerPage: u8, hits: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] struct HanimeSearchResult { id: u64, name: String, titles: Vec, slug: String, description: String, views: u64, interests: u64, poster_url: String, cover_url: String, brand: String, brand_id: u64, duration_in_ms: u32, is_censored: bool, rating: Option, likes: u64, dislikes: u64, downloads: u64, monthly_ranked: Option, tags: Vec, created_at: u64, released_at: u64, } #[derive(Debug, Clone)] pub struct HanimeProvider; impl HanimeProvider { pub fn new() -> Self { HanimeProvider } fn build_channel(&self, _clientversion: ClientVersion) -> Channel { Channel { id: "hanime".to_string(), name: "Hanime".to_string(), description: "Free Hentai from Hanime".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=hanime.tv".to_string(), status: "active".to_string(), categories: vec![], options: vec![ChannelOption { id: "sort".to_string(), title: "Sort".to_string(), description: "Sort the Videos".to_string(), systemImage: "list.number".to_string(), colorName: "blue".to_string(), options: vec![ FilterOption { id: "created_at_unix.desc".to_string(), title: "Recent Upload".to_string(), }, FilterOption { id: "created_at_unix.asc".to_string(), title: "Old Upload".to_string(), }, FilterOption { id: "views.desc".to_string(), title: "Most Views".to_string(), }, FilterOption { id: "views.asc".to_string(), title: "Least Views".to_string(), }, FilterOption { id: "likes.desc".to_string(), title: "Most Likes".to_string(), }, FilterOption { id: "likes.asc".to_string(), title: "Least Likes".to_string(), }, FilterOption { id: "released_at_unix.desc".to_string(), title: "New".to_string(), }, FilterOption { id: "released_at_unix.asc".to_string(), title: "Old".to_string(), }, FilterOption { id: "title_sortable.asc".to_string(), title: "A - Z".to_string(), }, FilterOption { id: "title_sortable.desc".to_string(), title: "Z - A".to_string(), }, ], multiSelect: false, }], nsfw: true, cacheDuration: None, } } 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 ); 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 ), &payload, vec![ ("Origin".to_string(), "https://hanime.tv".to_string()), ("Referer".to_string(), "https://hanime.tv/".to_string()), ], ) .await; // Initial request to set cookies ntex::time::sleep(ntex::time::Seconds(1)).await; let text = requester .get_raw_with_headers( &url, vec![ ("Origin".to_string(), "https://hanime.tv".to_string()), ("Referer".to_string(), "https://hanime.tv/".to_string()), ], ) .await .map_err(|e| { report_provider_error_background( "hanime", "get_video_item.get_raw_with_headers", &e.to_string(), ); Error::from(format!("Failed to fetch manifest response: {e}")) })? .text() .await .map_err(|e| { report_provider_error_background( "hanime", "get_video_item.response_text", &e.to_string(), ); Error::from(format!("Failed to decode manifest response body: {e}")) })?; if text.contains("Unautho") { println!("Fetched video details for {}: {}", title, text); return Err(Error::from("Unauthorized")); } let urls = 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(); 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, ); } Err(e) => { report_provider_error_background( "hanime", "get_video_item.insert_video.pool_get", &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(), )]), ) } async fn get( &self, cache: VideoCache, pool: DbPool, page: u8, query: String, sort: String, options: ServerOptions, ) -> Result> { let index = format!("hanime:{}:{}:{}", query, page, sort); let order_by = match sort.contains(".") { true => sort .split(".") .collect::>() .get(0) .copied() .unwrap_or_default() .to_string(), false => "created_at_unix".to_string(), }; let ordering = match sort.contains(".") { true => sort .split(".") .collect::>() .get(1) .copied() .unwrap_or_default() .to_string(), false => "desc".to_string(), }; let old_items = match cache.get(&index) { Some((time, items)) => { if time.elapsed().unwrap_or_default().as_secs() < 1 { //println!("Cache hit for URL: {}", index); return Ok(items.clone()); } else { items.clone() } } None => { vec![] } }; let search = HanimeSearchRequest::new() .page(page - 1) .search_text(query.clone()) .order_by(order_by) .ordering(ordering); let mut requester = crate::providers::requester_or_default(&options, module_path!(), "missing_requester"); let response = match requester .post_json("https://search.htv-services.com/search", &search, vec![]) .await { Ok(response) => response, Err(e) => { report_provider_error( "hanime", "get.search_request", &format!("query={query}; page={page}; error={e}"), ) .await; return Ok(old_items); } }; let hits = match response.json::().await { Ok(resp) => resp.hits, Err(e) => { println!("Failed to parse HanimeSearchResponse: {}", e); return Ok(old_items); } }; let hits_json: Vec = serde_json::from_str(hits.as_str()) .map_err(|e| format!("Failed to parse hits JSON: {}", e))?; // let timeout_duration = Duration::from_secs(120); let futures = hits_json .into_iter() .map(|el| self.get_video_item(el.clone(), pool.clone(), options.clone())); let results: Vec> = join_all(futures).await; let video_items: Vec = results.into_iter().filter_map(Result::ok).collect(); if !video_items.is_empty() { cache.remove(&index); cache.insert(index.clone(), video_items.clone()); } else { return Ok(old_items); } Ok(video_items) } } #[async_trait] impl Provider for HanimeProvider { async fn get_videos( &self, cache: VideoCache, pool: DbPool, sort: String, query: Option, page: String, per_page: String, options: ServerOptions, ) -> Vec { let _ = options; let _ = per_page; let _ = sort; let videos: std::result::Result, Error> = match query { Some(q) => { self.get( cache, pool, page.parse::().unwrap_or(1), q, sort, options, ) .await } None => { self.get( cache, pool, page.parse::().unwrap_or(1), "".to_string(), sort, options, ) .await } }; match videos { Ok(v) => v, Err(e) => { println!("Error fetching videos: {}", e); vec![] } } } fn get_channel(&self, clientversion: ClientVersion) -> Option { Some(self.build_channel(clientversion)) } }