diff --git a/src/api.rs b/src/api.rs index 7cc3081..14412dc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,9 @@ +use futures::channel; use ntex::http::header; use ntex::web; use ntex::web::HttpRequest; +use crate::providers::hanime::HanimeProvider; use crate::providers::perverzija::PerverzijaProvider; use crate::util::cache::VideoCache; use crate::{providers::*, status::*, videos::*}; @@ -158,6 +160,17 @@ async fn status(req: HttpRequest) -> Result { ], nsfw: true, }); + status.add_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![], + nsfw: true, + }); status.iconUrl = format!("http://{}/favicon.ico", host).to_string(); Ok(web::HttpResponse::Ok().json(&status)) } @@ -198,10 +211,19 @@ async fn videos_post( .parse() .unwrap(); let featured = video_request.featured.as_deref().unwrap_or("all").to_string(); - let provider = PerverzijaProvider::new(); + let provider = get_provider(channel.as_str()) + .ok_or_else(|| web::error::ErrorBadRequest("Invalid channel".to_string()))?; let video_items = provider .get_videos(cache.get_ref().clone(), channel, sort, query, page.to_string(), perPage.to_string(), featured) .await; videos.items = video_items.clone(); Ok(web::HttpResponse::Ok().json(&videos)) +} + +pub fn get_provider(channel: &str) -> Option { + match channel { + "perverzija" => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), + "hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())), + _ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), + } } \ No newline at end of file diff --git a/src/providers/hanime.rs b/src/providers/hanime.rs index 0c3ef23..559e4d8 100644 --- a/src/providers/hanime.rs +++ b/src/providers/hanime.rs @@ -1,10 +1,13 @@ +use std::time::Duration; use std::vec; use std::env; use error_chain::error_chain; use htmlentity::entity::{decode, ICodedDataTrait}; use reqwest::{Proxy}; +use futures::future::join_all; use crate::providers::Provider; +use crate::util::cache::VideoCache; use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr}; use crate::util::time::parse_time_to_seconds; use crate::videos::{self, Video_Embed, Video_Item}; // Make sure Provider trait is imported @@ -16,26 +19,119 @@ error_chain! { } } +#[derive(serde::Serialize, serde::Deserialize, Debug)] +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 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: f32, + likes: u64, + dislikes: u64, + downloads: u64, + monthly_ranked: Option, + tags: Vec, + created_at: u64, + released_at: u64, + +} + pub struct HanimeProvider { url: String, } + impl HanimeProvider { pub fn new() -> Self { HanimeProvider { url: "https://hanime.tv/".to_string(), } } - async fn get(&self, page: &u8, featured: String) -> Result> { - let mut prefix_uri = "".to_string(); - if featured == "featured" { - prefix_uri = "featured-scenes/".to_string(); - } - let mut url = format!("{}{}page/{}/", self.url, prefix_uri, page); - if page == &1 { - url = format!("{}{}", self.url, prefix_uri); - } - + async fn get_video_item(&self, hit: HanimeSearchResult) -> Result<(u64,Video_Item)> { + 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 + let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() @@ -48,49 +144,53 @@ impl HanimeProvider { .danger_accept_invalid_certs(true) .build()?, }; + let url = format!("https://h.freeanimehentai.net/api/v8/video?id={}&", hit.slug); + let response = client.get(url).send().await?; - let response = client.get(url.clone()).send().await?; - // print!("Response: {:?}\n", response); - if response.status().is_success() { - let text = response.text().await?; - let video_items: Vec = self.get_video_items_from_html(text.clone()); - Ok(video_items) - } else { - let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set"); - let flare = Flaresolverr::new(flare_url); - let result = flare - .solve(FlareSolverrRequest { - cmd: "request.get".to_string(), - url: url.clone(), - maxTimeout: 60000, - }) - .await; - println!("FlareSolverr result: {:?}", result); - let video_items = match result { - Ok(res) => { - // println!("FlareSolverr response: {}", res); - self.get_video_items_from_html(res.solution.response) - } - Err(e) => { - println!("Error solving FlareSolverr: {}", e); - return Err("Failed to solve FlareSolverr".into()); - } - }; - Ok(video_items) - } - } - async fn query(&self, page: &u8, query: &str) -> Result> { - println!("query: {}", query); - let search_string = query.replace(" ", "+"); - let mut url = format!( - "{}advanced-search/?_sf_s={}&sf_paged={}", - self.url, search_string, page - ); - if page == &1 { - url = format!("{}advanced-search/?_sf_s={}", self.url, search_string); - } + 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()); + } + } + Ok((hit.created_at, Video_Item::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::Video_Format::new(url_vec[0].clone(), "1080".to_string(), "m3u8".to_string())]))) + } + + async fn get(&self, cache: VideoCache, page: u8, query: String) -> Result> { + let index = format!("{}:{}", query, page); + + 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()); let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() @@ -103,241 +203,45 @@ impl HanimeProvider { .danger_accept_invalid_certs(true) .build()?, }; + let response = client.post("https://search.htv-services.com/search") + .json(&search) + .send().await?; + - let response = client.get(url.clone()).send().await?; - // print!("Response: {:?}\n", response); - if response.status().is_success() { - let text = response.text().await?; - let video_items: Vec = self.get_video_items_from_html_query(text.clone()); - Ok(video_items) - } else { - let flare_url = env::var("FLARE_URL").expect("FLARE_URL not set"); - let flare = Flaresolverr::new(flare_url); - let result = flare - .solve(FlareSolverrRequest { - cmd: "request.get".to_string(), - url: url.clone(), - maxTimeout: 60000, - }) - .await; - println!("FlareSolverr result: {:?}", result); - let video_items = match result { - Ok(res) => { - // println!("FlareSolverr response: {}", res); - self.get_video_items_from_html_query(res.solution.response) - } - Err(e) => { - println!("Error solving FlareSolverr: {}", e); - return Err("Failed to solve FlareSolverr".into()); - } - }; - Ok(video_items) - } - } - fn get_video_items_from_html(&self, html: String) -> Vec { - // println!("HTML: {}", html); - let mut items: Vec = Vec::new(); - let video_listing_content = html.split("video-listing-content").collect::>()[1]; - let raw_videos = video_listing_content - .split("video-item post") - .collect::>()[1..] - .to_vec(); - for video_segment in &raw_videos { - let vid = video_segment.split("\n").collect::>(); - if vid.len() > 20 { - println!("Skipping video segment with unexpected length: {}", vid.len()); - continue; + let hits = match response.json::().await { + Ok(resp) => resp.hits, + Err(e) => { + println!("Failed to parse HanimeSearchResponse: {}", e); + return Ok(old_items); } - for (i, line) in vid.iter().enumerate() { - if line.trim().is_empty() { - println!("Empty line at index {}: {}", i, line); - } - } - let mut title = vid[1].split(">").collect::>()[1] - .split("<") - .collect::>()[0] - .to_string(); - // html decode - title = decode(title.as_bytes()).to_string().unwrap_or(title); - let url = vid[1].split("iframe src="").collect::>()[1] - .split(""") - .collect::>()[0] - .to_string(); - let id = url.split("data=").collect::>()[1] - .split("&") - .collect::>()[0] - .to_string(); - let raw_duration = match vid.len() { - 10 => vid[6].split("time_dur\">").collect::>()[1] - .split("<") - .collect::>()[0] - .to_string(), - _ => "00:00".to_string(), - }; - let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; - - let thumb = match vid[4].contains("srcset=") { - true => vid[4].split("sizes=").collect::>()[1] - .split("w, ") - .collect::>() - .last() - .unwrap() - .to_string() - .split(" ") - .collect::>()[0] - .to_string(), - false => vid[4].split("src=\"").collect::>()[1] - .split("\"") - .collect::>()[0] - .to_string(), - }; - let embed_html = vid[1].split("data-embed='").collect::>()[1] - .split("'") - .collect::>()[0] - .to_string(); - let referer_url = vid[1].split("data-url='").collect::>()[1] - .split("'") - .collect::>()[0] - .to_string(); - let embed = Video_Embed::new(embed_html, url.clone()); - - let mut tags: Vec = Vec::new(); // Placeholder for tags, adjust as needed - for tag in vid[0].split(" ").collect::>(){ - if tag.starts_with("tag-") { - let tag_name = tag.split("tag-").collect::>()[1] - .to_string(); - if !tag_name.is_empty() { - tags.push(tag_name.replace("-", " ").to_string()); - } - } - } - let mut video_item = Video_Item::new( - id, - title, - url.clone(), - "hanime".to_string(), - thumb, - duration, - ).tags(tags) - .embed(embed.clone()); - let mut format = - videos::Video_Format::new(url.clone(), "1080".to_string(), "m3u8".to_string()); - format.add_http_header("Referer".to_string(), referer_url.clone()); - if let Some(formats) = video_item.formats.as_mut() { - formats.push(format); + }; + 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())); + let results: Vec> = join_all(futures).await; + let mut items: Vec<(u64, Video_Item)> = results + .into_iter() + .filter_map(Result::ok) + .collect(); + items.sort_by(|a, b| b.0.cmp(&a.0)); + let video_items: Vec = items.into_iter().map(|(_, item)| item).collect(); + if !video_items.is_empty() { + cache.remove(&index); + cache.insert(index.clone(), video_items.clone()); } else { - video_item.formats = Some(vec![format]); - } - items.push(video_item); - } - - return items; - } - - fn get_video_items_from_html_query(&self, html: String) -> Vec { - let mut items: Vec = Vec::new(); - let video_listing_content = html.split("search-filter-results-").collect::>()[1]; - let raw_videos = video_listing_content - .split("video-item post") - .collect::>()[1..] - .to_vec(); - for video_segment in &raw_videos { - let vid = video_segment.split("\n").collect::>(); - if vid.len() > 20 { - continue; - } - let mut title = vid[3].split("title='").collect::>()[1] - .split("'") - .collect::>()[0] - .to_string(); - title = decode(title.as_bytes()).to_string().unwrap_or(title); - let url = vid[4].split("iframe src="").collect::>()[1] - .split(""") - .collect::>()[0] - .to_string(); - let id = url.split("data=").collect::>()[1] - .split("&") - .collect::>()[0] - .to_string(); - let raw_duration = match vid.len() { - 18 => vid[16].split("time_dur\">").collect::>()[1] - .split("<") - .collect::>()[0] - .to_string(), - _ => "00:00".to_string(), - }; - let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; - let thumb_index = match vid.len() { - 18 => 14, - 13 => 8, - _ => { - continue; - } - }; - let thumb = match vid[thumb_index].contains("srcset=") { - true => vid[thumb_index].split("sizes=").collect::>()[1] - .split("w, ") - .collect::>() - .last() - .unwrap() - .to_string() - .split(" ") - .collect::>()[0] - .to_string(), - false => vid[thumb_index].split("src=\"").collect::>()[1] - .split("\"") - .collect::>()[0] - .to_string(), - }; - let embed_html = vid[4].split("data-embed='").collect::>()[1] - .split("'") - .collect::>()[0] - .to_string(); - let referer_url = vid[4].split("data-url='").collect::>()[1] - .split("'") - .collect::>()[0] - .to_string(); - let embed = Video_Embed::new(embed_html, url.clone()); - let mut tags: Vec = Vec::new(); // Placeholder for tags, adjust as needed - for tag in vid[0].split(" ").collect::>(){ - if tag.starts_with("tag-") { - let tag_name = tag.split("tag-").collect::>()[1] - .to_string(); - if !tag_name.is_empty() { - tags.push(tag_name.replace("-", " ").to_string()); - } - } + return Ok(old_items); } - let mut video_item = Video_Item::new( - id, - title, - url.clone(), - "hanime".to_string(), - thumb, - duration, - ) - .tags(tags) - .embed(embed.clone()); - let mut format = - videos::Video_Format::new(url.clone(), "1080".to_string(), "m3u8".to_string()); - format.add_http_header("Referer".to_string(), referer_url.clone()); - if let Some(formats) = video_item.formats.as_mut() { - formats.push(format); - } else { - video_item.formats = Some(vec![format]); - } - items.push(video_item); - } - - return items; + Ok(video_items) } } impl Provider for HanimeProvider { async fn get_videos( &self, + cache: VideoCache, _channel: String, sort: String, query: Option, @@ -345,11 +249,12 @@ impl Provider for HanimeProvider { per_page: String, featured: String, ) -> Vec { + let _ = featured; let _ = per_page; let _ = sort; let videos: std::result::Result, Error> = match query { - Some(q) => self.query(&page.parse::().unwrap_or(1), &q).await, - None => self.get(&page.parse::().unwrap_or(1), featured).await, + Some(q) => self.get(cache, page.parse::().unwrap_or(1), q).await, + None => self.get(cache, page.parse::().unwrap_or(1), "".to_string()).await, }; match videos { Ok(v) => v, diff --git a/src/providers/mod.rs b/src/providers/mod.rs index ceb6320..7e10a5f 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1,6 +1,21 @@ -use crate::{util::cache::VideoCache, videos::Video_Item}; +use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider}, util::cache::VideoCache, videos::Video_Item}; pub mod perverzija; +pub mod hanime; + pub trait Provider{ async fn get_videos(&self, cache: VideoCache ,channel: String, sort: String, query: Option, page: String, per_page: String, featured: String) -> Vec; -} \ No newline at end of file +} + +pub enum AnyProvider { + Perverzija(PerverzijaProvider), + Hanime(HanimeProvider), +} +impl Provider for AnyProvider { + async fn get_videos(&self, cache: VideoCache ,channel: String, sort: String, query: Option, page: String, per_page: String, featured: String) -> Vec { + match self { + AnyProvider::Perverzija(p) => p.get_videos(cache ,channel, sort, query, page, per_page, featured).await, + AnyProvider::Hanime(p) => p.get_videos(cache ,channel, sort, query, page, per_page, featured).await, + } + } +} diff --git a/src/providers/perverzija.rs b/src/providers/perverzija.rs index af4e72f..cddad5b 100644 --- a/src/providers/perverzija.rs +++ b/src/providers/perverzija.rs @@ -26,7 +26,7 @@ impl PerverzijaProvider { url: "https://tube.perverzija.com/".to_string(), } } - async fn get(&self, cache:VideoCache ,page: &u8, featured: String) -> Result> { + async fn get(&self, cache:VideoCache ,page: u8, featured: String) -> Result> { println!("get"); //TODO @@ -41,7 +41,7 @@ impl PerverzijaProvider { prefix_uri = "featured-scenes/".to_string(); } let mut url = format!("{}{}page/{}/", self.url, prefix_uri, page); - if page == &1 { + if page == 1 { url = format!("{}{}", self.url, prefix_uri); } @@ -116,14 +116,14 @@ impl PerverzijaProvider { Ok(video_items) } } - async fn query(&self, cache: VideoCache, page: &u8, query: &str) -> Result> { + async fn query(&self, cache: VideoCache, page: u8, query: &str) -> Result> { println!("query: {}", query); let search_string = query.replace(" ", "+"); let mut url = format!( "{}advanced-search/?_sf_s={}&sf_paged={}", self.url, search_string, page ); - if page == &1 { + if page == 1 { url = format!("{}advanced-search/?_sf_s={}", self.url, search_string); } @@ -216,11 +216,6 @@ impl PerverzijaProvider { println!("Skipping video segment with unexpected length: {}", vid.len()); continue; } - for (i, line) in vid.iter().enumerate() { - if line.trim().is_empty() { - println!("Empty line at index {}: {}", i, line); - } - } let mut title = vid[1].split(">").collect::>()[1] .split("<") .collect::>()[0] @@ -283,7 +278,7 @@ impl PerverzijaProvider { let mut video_item = Video_Item::new( id, title, - url.clone(), + embed.source.clone(), "perverzija".to_string(), thumb, duration, @@ -343,15 +338,8 @@ impl PerverzijaProvider { continue; } }; - let thumb = match vid[thumb_index].contains("srcset=") { - true => vid[thumb_index].split("sizes=").collect::>()[1] - .split("w, ") - .collect::>() - .last() - .unwrap() - .to_string() - .split(" ") - .collect::>()[0] + let thumb = match vid[thumb_index].contains("srcset=\"") { + true => vid[thumb_index].split(" ").collect::>()[0] .to_string(), false => vid[thumb_index].split("src=\"").collect::>()[1] .split("\"") @@ -418,8 +406,8 @@ impl Provider for PerverzijaProvider { let _ = per_page; let _ = sort; let videos: std::result::Result, Error> = match query { - Some(q) => self.query(cache, &page.parse::().unwrap_or(1), &q).await, - None => self.get(cache, &page.parse::().unwrap_or(1), featured).await, + Some(q) => self.query(cache, page.parse::().unwrap_or(1), &q).await, + None => self.get(cache, page.parse::().unwrap_or(1), featured).await, }; match videos { Ok(v) => v, diff --git a/src/videos.rs b/src/videos.rs index 744ed99..1ce2fe4 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -36,8 +36,8 @@ pub struct PageInfo { #[derive(serde::Serialize, Debug, Clone)] pub struct Video_Embed{ - html: String, - source: String, + pub html: String, + pub source: String, } impl Video_Embed { pub fn new(html: String, source: String) -> Self {