use ntex::http::header; use ntex::web; use ntex::web::HttpRequest; use std::cmp::Ordering; use tokio::task; use crate::providers::hanime::HanimeProvider; use crate::providers::perverzija::PerverzijaProvider; use crate::providers::pmvhaven::PmvhavenProvider; use crate::providers::pornhub::PornhubProvider; use crate::providers::spankbang::SpankbangProvider; use crate::util::cache::VideoCache; use crate::{DbPool, providers::*, status::*, videos::*}; #[derive(Debug)] struct ClientVersion { version: u32, subversion: u32, name: String, } impl ClientVersion { pub fn new(version: u32, subversion: u32, name: String) -> ClientVersion { ClientVersion { version, subversion, name, } } pub fn parse(input: &str) -> Option { // Example input: "Hot%20Tub/22c CFNetwork/1494.0.7 Darwin/23.4.0 0.002478" let parts: Vec<&str> = input.split_whitespace().collect(); if let Some(first) = parts.first() { let name_version: Vec<&str> = first.split('/').collect(); let name = name_version[1]; // Extract version and optional subversion let (version, subversion) = if let Some((v, c)) = name.split_at(name.len().saturating_sub(1)).into() { match v.parse::() { Ok(ver) => (ver, c.chars().next().map(|ch| ch as u32).unwrap_or(0)), Err(_) => { // Try parsing whole string if no subversion exists match name.parse::() { Ok(ver) => (ver, 0), Err(_) => return None, } } } } else { return None; }; return Some(ClientVersion { version: version, subversion: subversion, name: name.to_string(), }); } None } } // Implement comparisons impl PartialEq for ClientVersion { fn eq(&self, other: &Self) -> bool { self.name == other.name } } impl Eq for ClientVersion {} impl PartialOrd for ClientVersion { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for ClientVersion { fn cmp(&self, other: &Self) -> Ordering { self.version .cmp(&other.version) .then_with(|| self.subversion.cmp(&other.subversion)) } } pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::resource("/status") .route(web::post().to(status)) .route(web::get().to(status)), ) .service( web::resource("/videos") // .route(web::get().to(videos_get)) .route(web::post().to(videos_post)), ); } async fn status(req: HttpRequest) -> Result { let clientversion: ClientVersion = match req.headers().get("User-Agent") { Some(v) => match v.to_str() { Ok(useragent) => ClientVersion::parse(useragent) .unwrap_or_else(|| ClientVersion::new(999, 0, "999".to_string())), Err(_) => ClientVersion::new(999, 0, "999".to_string()), }, _ => ClientVersion::new(999, 0, "999".to_string()), }; let host = req .headers() .get(header::HOST) .and_then(|h| h.to_str().ok()) .unwrap_or_default() .to_string(); let mut status = Status::new(); // pronhub status.add_channel(Channel { id: "pornhub".to_string(), name: "Pornhub".to_string(), description: "Pornhub Free Videos".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=pornhub.com".to_string(), status: "active".to_string(), categories: vec![], options: vec![], nsfw: true, }); // pmvhaven status.add_channel(Channel { id: "pmvhaven".to_string(), name: "Pmvhaven".to_string(), description: "Explore a curated collection of captivating PMV".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(), status: "active".to_string(), categories: vec![], options: vec![ ChannelOption { id: "category".to_string(), title: "Category".to_string(), description: "Category of PMV Video get".to_string(), //"Sort the videos by Date or Name.".to_string(), systemImage: "list.number".to_string(), colorName: "blue".to_string(), options: vec![ FilterOption { id: "all".to_string(), title: "All".to_string(), }, FilterOption { id: "pmv".to_string(), title: "PMV".to_string(), }, FilterOption { id: "hmv".to_string(), title: "HMV".to_string(), }, FilterOption { id: "tiktok".to_string(), title: "Tiktok".to_string(), }, FilterOption { id: "koreanbj".to_string(), title: "KoreanBJ".to_string(), }, FilterOption { id: "hypno".to_string(), title: "Hypno".to_string(), }, FilterOption { id: "other".to_string(), title: "Other".to_string(), }, ], multiSelect: false, },], nsfw: true, }); if clientversion >= ClientVersion::new(22, 97, "22a".to_string()) { // perverzija status.add_channel(Channel { id: "perverzija".to_string(), name: "Perverzija".to_string(), description: "Free videos from Perverzija".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=tube.perverzija.com" .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(), //"Sort the videos by Date or Name.".to_string(), // systemImage: "list.number".to_string(), // colorName: "blue".to_string(), // options: vec![ // FilterOption { // id: "date".to_string(), // title: "Date".to_string(), // }, // FilterOption { // id: "name".to_string(), // title: "Name".to_string(), // }, // ], // multiSelect: false, // }, ChannelOption { id: "featured".to_string(), title: "Featured".to_string(), description: "Filter Featured Videos.".to_string(), systemImage: "star".to_string(), colorName: "red".to_string(), options: vec![ FilterOption { id: "all".to_string(), title: "No".to_string(), }, FilterOption { id: "featured".to_string(), title: "Yes".to_string(), }, ], multiSelect: false, }, // ChannelOption { // id: "duration".to_string(), // title: "Duration".to_string(), // description: "Filter the videos by duration.".to_string(), // systemImage: "timer".to_string(), // colorName: "green".to_string(), // options: vec![ // FilterOption { // id: "short".to_string(), // title: "< 1h".to_string(), // }, // FilterOption { // id: "long".to_string(), // title: "> 1h".to_string(), // }, // ], // multiSelect: true, // }, ], 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![ChannelOption { id: "sort".to_string(), title: "Sort".to_string(), description: "Sort the Videos".to_string(), //"Sort the videos by Date or Name.".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, }); status.add_channel(Channel { id: "spankbang".to_string(), name: "SpankBang".to_string(), description: "Popular Porn Videos - SpankBang".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=spankbang.com".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(), //"Sort the videos by Date or Name.".to_string(), systemImage: "list.number".to_string(), colorName: "blue".to_string(), options: vec![ FilterOption { id: "trending_videos".to_string(), title: "Trending".to_string(), }, FilterOption { id: "new_videos".to_string(), title: "New".to_string(), }, FilterOption { id: "most_popular".to_string(), title: "Popular".to_string(), }, ], multiSelect: false, }], nsfw: true, }); status.iconUrl = format!("http://{}/favicon.ico", host).to_string(); Ok(web::HttpResponse::Ok().json(&status)) } async fn videos_post( video_request: web::types::Json, cache: web::types::State, pool: web::types::State, ) -> Result { let mut videos = Videos { pageInfo: PageInfo { hasNextPage: true, resultsPerPage: 10, }, items: vec![], }; let channel: String = video_request .channel .as_deref() .unwrap_or("all") .to_string(); let sort: String = video_request.sort.as_deref().unwrap_or("date").to_string(); let mut query: Option = video_request.query.clone(); if video_request.query.as_deref() == Some("") { query = None; } let page: u8 = video_request .page .as_deref() .unwrap_or("1") .to_string() .parse() .unwrap(); let perPage: u8 = video_request .perPage .as_deref() .unwrap_or("10") .to_string() .parse() .unwrap(); let featured = video_request .featured .as_deref() .unwrap_or("all") .to_string(); let provider = get_provider(channel.as_str()) .ok_or_else(|| web::error::ErrorBadRequest("Invalid channel".to_string()))?; let category = video_request .category .as_deref() .unwrap_or("all") .to_string(); let video_items = provider .get_videos( cache.get_ref().clone(), pool.get_ref().clone(), channel.clone(), sort.clone(), query.clone(), page.to_string(), perPage.to_string(), featured.clone(), category.clone() ) .await; videos.items = video_items.clone(); if video_items.len() == 0 { videos.pageInfo = PageInfo { hasNextPage: false, resultsPerPage: 10, } } //### let next_page = page.to_string().parse::().unwrap_or(1) + 1; let provider_clone = provider.clone(); let cache_clone = cache.get_ref().clone(); let pool_clone = pool.get_ref().clone(); let channel_clone = channel.clone(); let sort_clone = sort.clone(); let query_clone = query.clone(); let per_page_clone = perPage.to_string(); let featured_clone = featured.clone(); let category_clone = category.clone(); task::spawn_local(async move { // if let AnyProvider::Spankbang(_) = provider_clone { // // Spankbang has a delay for the next page // ntex::time::sleep(ntex::time::Seconds(80)).await; // } let _ = provider_clone .get_videos( cache_clone, pool_clone, channel_clone, sort_clone, query_clone, next_page.to_string(), per_page_clone, featured_clone, category_clone, ) .await; }); //### 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())), "spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())), "pornhub" => Some(AnyProvider::Pornhub(PornhubProvider::new())), "pmvhaven" => Some(AnyProvider::Pmvhaven(PmvhavenProvider::new())), _ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), } }