use std::vec; use error_chain::error_chain; use futures::future::join_all; use wreq::Client; use wreq_util::Emulation; use crate::db; use crate::providers::Provider; use crate::util::cache::VideoCache; use crate::videos::{self, VideoItem}; use crate::DbPool; 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 } #[allow(dead_code)] 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 tags(mut self, tags: Vec) -> Self { self.tags = tags; self } pub fn search_text(mut self, search_text: String) -> Self { self.search_text = search_text; self } pub fn tags_mode(mut self, tags_mode: String) -> Self { self.tags_mode = tags_mode; self } pub fn brands(mut self, brands: Vec) -> Self { self.brands = brands; self } pub fn blacklist(mut self, blacklist: Vec) -> Self { self.blacklist = blacklist; 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)] #[allow(dead_code)] pub struct HanimeProvider { url: String, } impl HanimeProvider { pub fn new() -> Self { HanimeProvider { url: "https://hanime.tv/".to_string(), } } async fn get_video_item(&self, hit: HanimeSearchResult, pool: DbPool) -> Result { let mut conn = pool.get().expect("couldn't get db connection from pool"); 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 = hit.poster_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)) => { 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) .formats(vec![videos::VideoFormat::new(video_url.clone(), "1080".to_string(), "m3u8".to_string())])); } Ok(None) => (), Err(e) => { println!("Error fetching video from database: {}", e); // return Err(format!("Error fetching video from database: {}", e).into()); } } let client = Client::builder() .emulation(Emulation::Firefox136) .build()?; let url = format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug); let response = client.get(url).send().await?; let text = match response.status().is_success() { true => { response.text().await? }, false => { print!("Failed to fetch video item: {}\n\n", response.status()); return Err(format!("Failed to fetch video item: {}", response.status()).into()); } }; let urls = text.split("\"servers\"").collect::>()[1]; let mut url_vec = vec![]; for el in urls.split("\"url\":\"").collect::>(){ let url = el.split("\"").collect::>()[0]; if !url.is_empty() && url.contains("m3u8") { url_vec.push(url.to_string()); } } let mut conn = pool.get().expect("couldn't get db connection from pool"); let _ = db::insert_video(&mut conn, &format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug.clone()), &url_vec[0].clone()); drop(conn); Ok(VideoItem::new(id, title, url_vec[0].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(url_vec[0].clone(), "1080".to_string(), "m3u8".to_string())])) } async fn get(&self, cache: VideoCache, pool: DbPool, page: u8, query: String, sort:String) -> Result> { let index = format!("{}:{}:{}", query, page, sort); let order_by = match sort.contains("."){ true => sort.split(".").collect::>()[0].to_string(), false => "created_at_unix".to_string(), }; let ordering = match sort.contains("."){ true => sort.split(".").collect::>()[1].to_string(), false => "desc".to_string(), }; let old_items = match cache.get(&index) { Some((time, items)) => { if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 * 12 { 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 client = Client::builder() .emulation(Emulation::Firefox136) .build()?; let response = client.post("https://search.htv-services.com/search") .json(&search) .send().await?; 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())); 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) } } impl Provider for HanimeProvider { async fn get_videos( &self, cache: VideoCache, pool: DbPool, _channel: String, sort: String, query: Option, page: String, per_page: String, featured: String, ) -> Vec { let _ = featured; 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).await, None => self.get(cache, pool, page.parse::().unwrap_or(1), "".to_string(), sort).await, }; match videos { Ok(v) => v, Err(e) => { println!("Error fetching videos: {}", e); vec![] } } } }