use async_trait::async_trait; use futures::FutureExt; use once_cell::sync::Lazy; use rustc_hash::FxHashMap as HashMap; use std::future::Future; use std::panic::AssertUnwindSafe; use std::sync::Arc; use crate::{ DbPool, api::ClientVersion, status::{Channel, ChannelGroup, ChannelView, Status, StatusResponse}, util::{cache::VideoCache, discord::send_discord_error_report, requester::Requester}, videos::{ServerOptions, VideoItem}, }; pub mod all; pub mod hanime; pub mod homoxxx; pub mod okporn; pub mod okxxx; pub mod perfectgirls; pub mod perverzija; pub mod pmvhaven; pub mod pornhat; pub mod pornhub; pub mod redtube; pub mod rule34video; pub mod spankbang; // pub mod hentaimoon; pub mod beeg; pub mod missav; pub mod omgxxx; pub mod paradisehill; pub mod porn00; pub mod porn4fans; pub mod porndish; pub mod pornzog; pub mod shooshtime; pub mod sxyprn; pub mod tnaflix; pub mod tokyomotion; pub mod viralxxxporn; pub mod vrporn; pub mod xfree; pub mod xxthots; pub mod yesporn; pub mod youjizz; // pub mod pornxp; pub mod chaturbate; pub mod freepornvideosxxx; pub mod heavyfetish; pub mod hsex; pub mod hentaihaven; pub mod hqporner; pub mod hypnotube; pub mod javtiful; pub mod noodlemagazine; pub mod pimpbunny; pub mod rule34gen; pub mod xxdbx; // pub mod tube8; // convenient alias pub type DynProvider = Arc; #[derive(Clone, Copy)] pub struct ProviderChannelMetadata { pub group_id: &'static str, pub tags: &'static [&'static str], } pub static ALL_PROVIDERS: Lazy> = Lazy::new(|| { let mut m = HashMap::default(); m.insert("all", Arc::new(all::AllProvider::new()) as DynProvider); m.insert( "perverzija", Arc::new(perverzija::PerverzijaProvider::new()) as DynProvider, ); m.insert( "hanime", Arc::new(hanime::HanimeProvider::new()) as DynProvider, ); m.insert( "pornhub", Arc::new(pornhub::PornhubProvider::new()) as DynProvider, ); m.insert( "spankbang", Arc::new(spankbang::SpankbangProvider::new()) as DynProvider, ); m.insert( "rule34video", Arc::new(rule34video::Rule34videoProvider::new()) as DynProvider, ); m.insert( "redtube", Arc::new(redtube::RedtubeProvider::new()) as DynProvider, ); m.insert( "okporn", Arc::new(okporn::OkpornProvider::new()) as DynProvider, ); m.insert( "pornhat", Arc::new(pornhat::PornhatProvider::new()) as DynProvider, ); m.insert( "perfectgirls", Arc::new(perfectgirls::PerfectgirlsProvider::new()) as DynProvider, ); m.insert( "okxxx", Arc::new(okxxx::OkxxxProvider::new()) as DynProvider, ); m.insert( "homoxxx", Arc::new(homoxxx::HomoxxxProvider::new()) as DynProvider, ); m.insert( "missav", Arc::new(missav::MissavProvider::new()) as DynProvider, ); m.insert( "xxthots", Arc::new(xxthots::XxthotsProvider::new()) as DynProvider, ); m.insert( "yesporn", Arc::new(yesporn::YespornProvider::new()) as DynProvider, ); m.insert( "sxyprn", Arc::new(sxyprn::SxyprnProvider::new()) as DynProvider, ); m.insert( "porn00", Arc::new(porn00::Porn00Provider::new()) as DynProvider, ); m.insert( "youjizz", Arc::new(youjizz::YoujizzProvider::new()) as DynProvider, ); m.insert( "paradisehill", Arc::new(paradisehill::ParadisehillProvider::new()) as DynProvider, ); m.insert( "porn4fans", Arc::new(porn4fans::Porn4fansProvider::new()) as DynProvider, ); m.insert( "porndish", Arc::new(porndish::PorndishProvider::new()) as DynProvider, ); m.insert( "shooshtime", Arc::new(shooshtime::ShooshtimeProvider::new()) as DynProvider, ); m.insert( "pornzog", Arc::new(pornzog::PornzogProvider::new()) as DynProvider, ); m.insert( "omgxxx", Arc::new(omgxxx::OmgxxxProvider::new()) as DynProvider, ); m.insert("beeg", Arc::new(beeg::BeegProvider::new()) as DynProvider); m.insert( "tnaflix", Arc::new(tnaflix::TnaflixProvider::new()) as DynProvider, ); m.insert( "tokyomotion", Arc::new(tokyomotion::TokyomotionProvider::new()) as DynProvider, ); m.insert( "viralxxxporn", Arc::new(viralxxxporn::ViralxxxpornProvider::new()) as DynProvider, ); m.insert( "vrporn", Arc::new(vrporn::VrpornProvider::new()) as DynProvider, ); // m.insert("pornxp", Arc::new(pornxp::PornxpProvider::new()) as DynProvider); m.insert( "rule34gen", Arc::new(rule34gen::Rule34genProvider::new()) as DynProvider, ); m.insert( "xxdbx", Arc::new(xxdbx::XxdbxProvider::new()) as DynProvider, ); m.insert( "xfree", Arc::new(xfree::XfreeProvider::new()) as DynProvider, ); m.insert( "hqporner", Arc::new(hqporner::HqpornerProvider::new()) as DynProvider, ); m.insert( "pmvhaven", Arc::new(pmvhaven::PmvhavenProvider::new()) as DynProvider, ); m.insert( "noodlemagazine", Arc::new(noodlemagazine::NoodlemagazineProvider::new()) as DynProvider, ); m.insert( "pimpbunny", Arc::new(pimpbunny::PimpbunnyProvider::new()) as DynProvider, ); m.insert( "javtiful", Arc::new(javtiful::JavtifulProvider::new()) as DynProvider, ); m.insert( "hypnotube", Arc::new(hypnotube::HypnotubeProvider::new()) as DynProvider, ); m.insert( "freepornvideosxxx", Arc::new(freepornvideosxxx::FreepornvideosxxxProvider::new()) as DynProvider, ); m.insert( "heavyfetish", Arc::new(heavyfetish::HeavyfetishProvider::new()) as DynProvider, ); m.insert("hsex", Arc::new(hsex::HsexProvider::new()) as DynProvider); m.insert( "hentaihaven", Arc::new(hentaihaven::HentaihavenProvider::new()) as DynProvider, ); m.insert( "chaturbate", Arc::new(chaturbate::ChaturbateProvider::new()) as DynProvider, ); // m.insert("tube8", Arc::new(tube8::Tube8Provider::new()) as DynProvider); // add more here as you migrate them m }); pub fn init_providers_now() { // Idempotent & thread-safe: runs the Lazy init exactly once. Lazy::force(&ALL_PROVIDERS); } pub fn panic_payload_to_string(payload: Box) -> String { if let Some(s) = payload.downcast_ref::<&str>() { return (*s).to_string(); } if let Some(s) = payload.downcast_ref::() { return s.clone(); } "unknown panic payload".to_string() } pub async fn run_provider_guarded(provider_name: &str, context: &str, fut: F) -> Vec where F: Future>, { match AssertUnwindSafe(fut).catch_unwind().await { Ok(videos) => videos, Err(payload) => { let panic_msg = panic_payload_to_string(payload); let _ = send_discord_error_report( format!("Provider panic: {}", provider_name), None, Some("Provider Guard"), Some(&format!("context={}; panic={}", context, panic_msg)), file!(), line!(), module_path!(), ) .await; vec![] } } } pub async fn report_provider_error(provider_name: &str, context: &str, msg: &str) { let _ = send_discord_error_report( format!("Provider error: {}", provider_name), None, Some("Provider Guard"), Some(&format!("context={}; error={}", context, msg)), file!(), line!(), module_path!(), ) .await; } pub fn report_provider_error_background(provider_name: &str, context: &str, msg: &str) { let provider_name = provider_name.to_string(); let context = context.to_string(); let msg = msg.to_string(); tokio::spawn(async move { report_provider_error(&provider_name, &context, &msg).await; }); } pub fn requester_or_default( options: &ServerOptions, provider_name: &str, context: &str, ) -> Requester { match options.requester.clone() { Some(requester) => requester, None => { report_provider_error_background( provider_name, context, "ServerOptions.requester missing; using default Requester", ); Requester::new() } } } pub fn strip_url_scheme(url: &str) -> String { url.strip_prefix("https://") .or_else(|| url.strip_prefix("http://")) .unwrap_or(url) .trim_start_matches('/') .to_string() } pub fn build_proxy_url(options: &ServerOptions, proxy: &str, target: &str) -> String { let target = target.trim_start_matches('/'); let base = options .public_url_base .as_deref() .unwrap_or("") .trim_end_matches('/'); if base.is_empty() { format!("/proxy/{proxy}/{target}") } else { format!("{base}/proxy/{proxy}/{target}") } } fn channel_metadata_for(id: &str) -> Option { match id { "all" | "hottub" => Some(all::CHANNEL_METADATA), "pornhub" => Some(pornhub::CHANNEL_METADATA), "spankbang" => Some(spankbang::CHANNEL_METADATA), "rule34video" => Some(rule34video::CHANNEL_METADATA), "redtube" => Some(redtube::CHANNEL_METADATA), "okporn" => Some(okporn::CHANNEL_METADATA), "pornhat" => Some(pornhat::CHANNEL_METADATA), "perfectgirls" => Some(perfectgirls::CHANNEL_METADATA), "okxxx" => Some(okxxx::CHANNEL_METADATA), "homoxxx" => Some(homoxxx::CHANNEL_METADATA), "missav" => Some(missav::CHANNEL_METADATA), "xxthots" => Some(xxthots::CHANNEL_METADATA), "yesporn" => Some(yesporn::CHANNEL_METADATA), "sxyprn" => Some(sxyprn::CHANNEL_METADATA), "porn00" => Some(porn00::CHANNEL_METADATA), "youjizz" => Some(youjizz::CHANNEL_METADATA), "paradisehill" => Some(paradisehill::CHANNEL_METADATA), "porn4fans" => Some(porn4fans::CHANNEL_METADATA), "porndish" => Some(porndish::CHANNEL_METADATA), "shooshtime" => Some(shooshtime::CHANNEL_METADATA), "pornzog" => Some(pornzog::CHANNEL_METADATA), "omgxxx" => Some(omgxxx::CHANNEL_METADATA), "beeg" => Some(beeg::CHANNEL_METADATA), "tnaflix" => Some(tnaflix::CHANNEL_METADATA), "tokyomotion" => Some(tokyomotion::CHANNEL_METADATA), "viralxxxporn" => Some(viralxxxporn::CHANNEL_METADATA), "vrporn" => Some(vrporn::CHANNEL_METADATA), "rule34gen" => Some(rule34gen::CHANNEL_METADATA), "xxdbx" => Some(xxdbx::CHANNEL_METADATA), "xfree" => Some(xfree::CHANNEL_METADATA), "hqporner" => Some(hqporner::CHANNEL_METADATA), "pmvhaven" => Some(pmvhaven::CHANNEL_METADATA), "noodlemagazine" => Some(noodlemagazine::CHANNEL_METADATA), "pimpbunny" => Some(pimpbunny::CHANNEL_METADATA), "javtiful" => Some(javtiful::CHANNEL_METADATA), "hypnotube" => Some(hypnotube::CHANNEL_METADATA), "freepornvideosxxx" => Some(freepornvideosxxx::CHANNEL_METADATA), "heavyfetish" => Some(heavyfetish::CHANNEL_METADATA), "hsex" => Some(hsex::CHANNEL_METADATA), "hentaihaven" => Some(hentaihaven::CHANNEL_METADATA), "hanime" => Some(hanime::CHANNEL_METADATA), "perverzija" => Some(perverzija::CHANNEL_METADATA), "chaturbate" => Some(chaturbate::CHANNEL_METADATA), _ => None, } } fn channel_group_title(group_id: &str) -> &'static str { match group_id { "meta-search" => "Meta Search", "mainstream-tube" => "Mainstream Tube", "studio-network" => "Studio & Network", "amateur-homemade" => "Amateur & Homemade", "creator-leaks" => "Creator & Leaks", "asian-jav" => "Asian & JAV", "fetish-kink" => "Fetish & Kink", "hentai-animation" => "Hentai & Animation", "gay-male" => "Gay & Male", "live-cams" => "Live Cams", "pmv-compilation" => "PMV & Compilation", _ => "Other", } } fn channel_group_order(group_id: &str) -> usize { match group_id { "meta-search" => 0, "mainstream-tube" => 1, "studio-network" => 2, "amateur-homemade" => 3, "creator-leaks" => 4, "asian-jav" => 5, "fetish-kink" => 6, "hentai-animation" => 7, "gay-male" => 8, "live-cams" => 9, "pmv-compilation" => 10, _ => 99, } } pub fn decorate_channel(channel: Channel) -> ChannelView { let metadata = channel_metadata_for(&channel.id); ChannelView { id: channel.id, name: channel.name, description: channel.description, premium: channel.premium, favicon: channel.favicon, status: channel.status, categories: channel.categories, options: channel.options, nsfw: channel.nsfw, group: metadata.map(|value| value.group_id.to_string()), tags: metadata.map(|value| { value .tags .iter() .take(3) .map(|tag| (*tag).to_string()) .collect() }), cacheDuration: channel.cacheDuration, } } pub fn build_channel_groups(channels: &[ChannelView]) -> Vec { let mut groups = Vec::new(); let mut group_ids = channels .iter() .filter_map(|channel| channel.group.clone()) .collect::>(); group_ids.sort_by_key(|group_id| (channel_group_order(group_id), group_id.clone())); group_ids.dedup(); for group_id in group_ids { let mut channel_ids = channels .iter() .filter(|channel| channel.group.as_deref() == Some(group_id.as_str())) .map(|channel| channel.id.clone()) .collect::>(); channel_ids.sort(); groups.push(ChannelGroup { id: group_id.clone(), title: channel_group_title(&group_id).to_string(), channels: channel_ids, }); } groups } pub fn build_status_response(status: Status) -> StatusResponse { let channels = status .channels .into_iter() .map(decorate_channel) .collect::>(); let channelGroups = build_channel_groups(&channels); StatusResponse { id: status.id, name: status.name, subtitle: status.subtitle, description: status.description, iconUrl: status.iconUrl, color: status.color, status: status.status, notices: status.notices, channels, channelGroups, subscription: status.subscription, nsfw: status.nsfw, categories: status.categories, options: status.options, filtersFooter: status.filtersFooter, } } #[async_trait] pub trait Provider: Send + Sync { async fn get_videos( &self, cache: VideoCache, pool: DbPool, sort: String, query: Option, page: String, per_page: String, options: ServerOptions, ) -> Vec; fn get_channel(&self, clientversion: ClientVersion) -> Option { println!( "Getting channel for placeholder with client version: {:?}", clientversion ); let _ = clientversion; Some(Channel { id: "placeholder".to_string(), name: "PLACEHOLDER".to_string(), description: "PLACEHOLDER FOR PARENT CLASS".to_string(), premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=missav.ws".to_string(), status: "active".to_string(), categories: vec![], options: vec![], nsfw: true, cacheDuration: None, }) } } #[cfg(test)] mod tests { use super::*; use crate::status::ChannelOption; fn base_channel(id: &str) -> Channel { Channel { id: id.to_string(), name: id.to_string(), description: String::new(), premium: false, favicon: String::new(), status: "active".to_string(), categories: vec![], options: Vec::::new(), nsfw: true, cacheDuration: None, } } #[test] fn decorates_channel_with_group_and_tags() { let channel = decorate_channel(base_channel("hsex")); assert_eq!(channel.group.as_deref(), Some("amateur-homemade")); assert_eq!( channel.tags.as_deref(), Some(&[ "amateur".to_string(), "chinese".to_string(), "homemade".to_string(), ][..]) ); } #[test] fn builds_group_index() { let channels = vec![ decorate_channel(base_channel("all")), decorate_channel(base_channel("hsex")), decorate_channel(base_channel("missav")), ]; let groups = build_channel_groups(&channels); assert_eq!(groups[0].id, "meta-search"); assert_eq!(groups[1].id, "amateur-homemade"); assert_eq!(groups[2].id, "asian-jav"); } }