From 2f1fd8f33a483f3c540a4c15c1b784ed2f220706 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 10 Feb 2026 20:24:10 +0000 Subject: [PATCH] freepornvideosxxx --- src/providers/freepornvideosxxx.rs | 627 +++++++++++++++++++++++++++++ src/providers/mod.rs | 3 +- 2 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 src/providers/freepornvideosxxx.rs diff --git a/src/providers/freepornvideosxxx.rs b/src/providers/freepornvideosxxx.rs new file mode 100644 index 0000000..423f527 --- /dev/null +++ b/src/providers/freepornvideosxxx.rs @@ -0,0 +1,627 @@ +use crate::DbPool; +use crate::api::ClientVersion; +use crate::providers::Provider; +use crate::util::cache::VideoCache; +use crate::util::parse_abbreviated_number; +use crate::util::time::parse_time_to_seconds; +use crate::videos::{ServerOptions, VideoItem}; +use crate::{status::*, util}; +use async_trait::async_trait; +use error_chain::error_chain; +use htmlentity::entity::{ICodedDataTrait, decode}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::vec; + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(wreq::Error); + } +} + +#[derive(Debug, Clone)] +pub struct FreepornvideosxxxProvider { + url: String, + sites: Arc>>, + networks: Arc>>, + stars: Arc>>, +} +impl FreepornvideosxxxProvider { + pub fn new() -> Self { + let provider = FreepornvideosxxxProvider { + url: "https://www.freepornvideos.xxx".to_string(), + sites: Arc::new(RwLock::new(vec![FilterOption { + id: "all".to_string(), + title: "All".to_string(), + }])), + networks: Arc::new(RwLock::new(vec![FilterOption { + id: "all".to_string(), + title: "All".to_string(), + }])), + stars: Arc::new(RwLock::new(vec![FilterOption { + id: "all".to_string(), + title: "All".to_string(), + }])), + }; + + // Kick off the background load but return immediately + provider.spawn_initial_load(); + provider + } + + fn spawn_initial_load(&self) { + let url = self.url.clone(); + let sites = Arc::clone(&self.sites); + let networks = Arc::clone(&self.networks); + let stars = Arc::clone(&self.stars); + + thread::spawn(move || { + // Create a tiny runtime just for these async tasks + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("build tokio runtime"); + + rt.block_on(async move { + // If you have a streaming sites loader, call it here too + if let Err(e) = Self::load_sites(&url, sites).await { + eprintln!("load_sites_into failed: {e}"); + } + + if let Err(e) = Self::load_networks(&url, networks).await { + eprintln!("load_networks failed: {e}"); + } + + if let Err(e) = Self::load_stars(&url, stars).await { + eprintln!("load_stars failed: {e}"); + } + }); + }); + } + + async fn load_stars(base_url: &str, stars: Arc>>) -> Result<()> { + let mut requester = util::requester::Requester::new(); + for page in [1..10].into_iter().flatten() { + let text = requester + .get( + format!("{}/models/total-videos/{}/?gender_id=0", &base_url, page).as_str(), + None, + ) + .await + .unwrap(); + if text.contains("404 Not Found") || text.is_empty() { + break; + } + let stars_div = text + .split("
") + .collect::>() + .last() + .unwrap() + .split("custom_list_models_models_list_pagination") + .collect::>()[0]; + for stars_element in stars_div.split(">()[1..].to_vec() { + let star_url = stars_element.split("href=\"").collect::>()[1] + .split("\"") + .collect::>()[0]; + let star_id = star_url.split("/").collect::>()[4].to_string(); + let star_name = stars_element + .split("") + .collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + Self::push_unique( + &stars, + FilterOption { + id: star_id, + title: star_name, + }, + ); + } + } + return Ok(()); + } + + async fn load_sites(base_url: &str, sites: Arc>>) -> Result<()> { + let mut requester = util::requester::Requester::new(); + let mut page = 0; + loop { + page += 1; + let text = requester + .get(format!("{}/sites/{}/", &base_url, page).as_str(), None) + .await + .unwrap(); + if text.contains("404 Not Found") || text.is_empty() { + break; + } + let sites_div = text + .split("id=\"list_content_sources_sponsors_list_items\"") + .collect::>()[1] + .split("class=\"pagination\"") + .collect::>()[0]; + for sites_element in + sites_div.split("class=\"headline\"").collect::>()[1..].to_vec() + { + let site_url = sites_element.split("href=\"").collect::>()[1] + .split("\"") + .collect::>()[0]; + let site_id = site_url.split("/").collect::>()[4].to_string(); + let site_name = sites_element.split("

").collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + Self::push_unique( + &sites, + FilterOption { + id: site_id, + title: site_name, + }, + ); + } + } + return Ok(()); + } + + async fn load_networks(base_url: &str, networks: Arc>>) -> Result<()> { + let mut requester = util::requester::Requester::new(); + let text = requester.get(&base_url, None).await.unwrap(); + let networks_div = text.split("class=\"sites__list\"").collect::>()[1] + .split("

") + .collect::>()[0]; + for network_element in + networks_div.split("sites__item").collect::>()[1..].to_vec() + { + if network_element.contains("sites__all") { + continue; + } + let network_url = network_element.split("href=\"").collect::>()[1] + .split("\"") + .collect::>()[0]; + let network_id = network_url.split("/").collect::>()[4].to_string(); + let network_name = network_element.split(">").collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + Self::push_unique( + &networks, + FilterOption { + id: network_id, + title: network_name, + }, + ); + } + return Ok(()); + } + + // Push one item with minimal lock time and dedup by id + fn push_unique(target: &Arc>>, item: FilterOption) { + if let Ok(mut vec) = target.write() { + if !vec.iter().any(|x| x.id == item.id) { + vec.push(item); + // Optional: keep it sorted for nicer UX + // vec.sort_by(|a,b| a.title.cmp(&b.title)); + } + } + } + + fn build_channel(&self, clientversion: ClientVersion) -> Channel { + let _ = clientversion; + let sites: Vec = self + .sites + .read() + .map(|g| g.clone()) // or: .map(|g| g.to_vec()) + .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) + + let networks: Vec = self + .networks + .read() + .map(|g| g.clone()) // or: .map(|g| g.to_vec()) + .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) + + let stars: Vec = self + .stars + .read() + .map(|g| g.clone()) // or: .map(|g| g.to_vec()) + .unwrap_or_default(); // or: .unwrap_or_else(|_| Vec::new()) + + Channel { + id: "freepornvideosxxx".to_string(), + name: "FreePornVideos XXX".to_string(), + description: "Free Porn Videos".to_string(), + premium: false, + favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.freepornvideos.xxx".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: "latest-updates".into(), + title: "Latest".into(), + }, + FilterOption { + id: "most-popular".into(), + title: "Most Viewed".into(), + }, + FilterOption { + id: "top-rated".into(), + title: "Top Rated".into(), + }, + ], + multiSelect: false, + }, + ChannelOption { + id: "sites".to_string(), + title: "Sites".to_string(), + description: "Filter for different Sites".to_string(), + systemImage: "rectangle.stack".to_string(), + colorName: "green".to_string(), + options: sites, + multiSelect: false, + }, + ChannelOption { + id: "networks".to_string(), + title: "Networks".to_string(), + description: "Filter for different Networks".to_string(), + systemImage: "list.dash".to_string(), + colorName: "purple".to_string(), + options: networks, + multiSelect: false, + }, + ChannelOption { + id: "stars".to_string(), + title: "Stars".to_string(), + description: "Filter for different Pornstars".to_string(), + systemImage: "star.fill".to_string(), + colorName: "yellow".to_string(), + options: stars, + multiSelect: false, + }, + ], + nsfw: true, + cacheDuration: None, + } + } + + async fn get( + &self, + cache: VideoCache, + page: u8, + sort: &str, + options: ServerOptions, + ) -> Result> { + let mut sort_string: String = match sort { + "top-rated" => "top-rated".to_string(), + "most-popular" => "most-popular".to_string(), + _ => "latest-updates".to_string(), + }; + let alt_sort_string: String = match sort { + "top-rated" => "/top-rated".to_string(), + "most-popular" => "/most-popular".to_string(), + _ => "".to_string(), + }; + if options.network.is_some() + && !options.network.as_ref().unwrap().is_empty() + && options.network.as_ref().unwrap() != "all" + { + sort_string = format!( + "networks/{}{}", + options.network.as_ref().unwrap(), + alt_sort_string + ); + } + if options.sites.is_some() + && !options.sites.as_ref().unwrap().is_empty() + && options.sites.as_ref().unwrap() != "all" + { + sort_string = format!( + "sites/{}{}", + options.sites.as_ref().unwrap(), + alt_sort_string + ); + } + if options.stars.is_some() + && !options.stars.as_ref().unwrap().is_empty() + && options.stars.as_ref().unwrap() != "all" + { + sort_string = format!( + "models/{}{}", + options.stars.as_ref().unwrap(), + alt_sort_string + ); + } + let video_url = format!("{}/{}/{}/", self.url, sort_string, page); + let old_items = match cache.get(&video_url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { + return Ok(items.clone()); + } else { + items.clone() + } + } + None => { + vec![] + } + }; + + let mut requester = options.requester.clone().unwrap(); + let text = requester.get(&video_url, None).await.unwrap(); + let video_items: Vec = self.get_video_items_from_html(text.clone()); + if !video_items.is_empty() { + cache.remove(&video_url); + cache.insert(video_url.clone(), video_items.clone()); + } else { + return Ok(old_items); + } + Ok(video_items) + } + + async fn query( + &self, + cache: VideoCache, + page: u8, + query: &str, + options: ServerOptions, + ) -> Result> { + let mut search_type = "search"; + let mut search_string = query.to_string().to_ascii_lowercase().trim().to_string(); + match self + .stars + .read() + .unwrap() + .iter() + .find(|s| s.title.to_ascii_lowercase() == search_string) + { + Some(star) => { + search_type = "models"; + search_string = star.id.clone(); + } + _ => {} + } + match self + .sites + .read() + .unwrap() + .iter() + .find(|s| s.title.to_ascii_lowercase() == search_string) + { + Some(site) => { + search_type = "sites"; + search_string = site.id.clone(); + } + _ => {} + } + let mut video_url = format!("{}/{}/{}/{}/", self.url, search_type, search_string, page); + video_url = video_url.replace(" ", "+"); + // Check our Video Cache. If the result is younger than 1 hour, we return it. + let old_items = match cache.get(&video_url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { + return Ok(items.clone()); + } else { + let _ = cache.check().await; + return Ok(items.clone()); + } + } + None => { + vec![] + } + }; + + let mut requester = options.requester.clone().unwrap(); + + let text = requester.get(&video_url, None).await.unwrap(); + let video_items: Vec = self.get_video_items_from_html(text.clone()); + if !video_items.is_empty() { + cache.remove(&video_url); + cache.insert(video_url.clone(), video_items.clone()); + } else { + return Ok(old_items); + } + Ok(video_items) + } + + fn get_site_id_from_name(&self, site_name: &str) -> Option { + // site_name.to_lowercase().replace(" ", "") + for site in self.sites.read().unwrap().iter() { + if site + .title + .to_lowercase() + .replace(" ", "") + .replace(".com", "") + == site_name.to_lowercase().replace(" ", "") + { + return Some(site.id.clone()); + } + } + return None; + } + + fn get_video_items_from_html(&self, html: String) -> Vec { + if html.is_empty() { + println!("HTML is empty"); + return vec![]; + } + let mut items: Vec = Vec::new(); + if !html.contains("class=\"item\"") { + return items; + } + let raw_videos = html.split("videos_list_pagination").collect::>()[0] + .split(" class=\"pagination\" ") + .collect::>()[0] + .split("class=\"list-videos\"") + .collect::>()[1] + .split("class=\"item\"") + .collect::>()[1..] + .to_vec(); + for video_segment in &raw_videos { + // let vid = video_segment.split("\n").collect::>(); + // for (index, line) in vid.iter().enumerate() { + // println!("Line {}: {}", index, line); + // } + let video_url: String = video_segment.split(">()[1] + .split("\"") + .collect::>()[0] + .to_string(); + let mut title = video_segment.split(" title=\"").collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(); + // html decode + title = decode(title.as_bytes()).to_string().unwrap_or(title); + let id = video_url.split("/").collect::>()[4].to_string(); + + let thumb = match video_segment.split(">()[1] + .contains("data-src=\"") + { + true => video_segment.split(">()[1] + .split("data-src=\"") + .collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(), + false => video_segment.split(">()[1] + .split("src=\"") + .collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(), + }; + let raw_duration = video_segment + .split("") + .collect::>()[1] + .split("<") + .collect::>()[0] + .split(" ") + .collect::>() + .last() + .unwrap_or(&"") + .to_string(); + let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32; + let views = parse_abbreviated_number( + video_segment + .split("
") + .collect::>()[1] + .split("<") + .collect::>()[0] + .to_string() + .as_str(), + ) + .unwrap_or(0) as u32; + + let preview = video_segment + .split("data-preview=\"") + .collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(); + let site_name = title + .split("]") + .collect::>() + .first() + .unwrap_or(&"") + .trim_start_matches("["); + let site_id = self + .get_site_id_from_name(site_name) + .unwrap_or("".to_string()); + let mut tags = match video_segment.contains("class=\"models\">") { + true => video_segment + .split("class=\"models\">") + .collect::>()[1] + .split("
") + .collect::>()[0] + .split("href=\"") + .collect::>()[1..] + .into_iter() + .map(|s| { + Self::push_unique( + &self.stars, + FilterOption { + id: s.split("/").collect::>()[4].to_string(), + title: s.split(">").collect::>()[1] + .split("<") + .collect::>()[0] + .trim() + .to_string(), + }, + ); + s.split(">").collect::>()[1] + .split("<") + .collect::>()[0] + .trim() + .to_string() + }) + .collect::>() + .to_vec(), + false => vec![], + }; + if !site_id.is_empty() { + Self::push_unique( + &self.sites, + FilterOption { + id: site_id, + title: site_name.to_string(), + }, + ); + tags.push(site_name.to_string()); + } + + let video_item = VideoItem::new( + id, + title, + video_url.to_string(), + "freepornvideosxxx".to_string(), + thumb, + duration, + ) + .views(views) + .preview(preview) + .tags(tags); + items.push(video_item); + } + return items; + } +} + +#[async_trait] +impl Provider for FreepornvideosxxxProvider { + async fn get_videos( + &self, + cache: VideoCache, + pool: DbPool, + sort: String, + query: Option, + page: String, + per_page: String, + options: ServerOptions, + ) -> Vec { + let _ = per_page; + let _ = pool; + let videos: std::result::Result, Error> = match query { + Some(q) => { + self.query(cache, page.parse::().unwrap_or(1), &q, options) + .await + } + None => { + self.get(cache, page.parse::().unwrap_or(1), &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)) + } +} diff --git a/src/providers/mod.rs b/src/providers/mod.rs index fd855ce..6f5c147 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -41,7 +41,7 @@ pub mod noodlemagazine; pub mod pimpbunny; pub mod javtiful; pub mod hypnotube; - +pub mod freepornvideosxxx; // convenient alias pub type DynProvider = Arc; @@ -59,6 +59,7 @@ pub static ALL_PROVIDERS: Lazy> = Lazy::new(| 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); // add more here as you migrate them m });