From 53a4c62bfe595738867b41a065939c71dec70ef8 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 29 Aug 2025 19:36:33 +0000 Subject: [PATCH] porn00 --- Cargo.toml | 1 + src/api.rs | 36 ++++++ src/main.rs | 2 + src/providers/mod.rs | 12 ++ src/providers/porn00.rs | 244 +++++++++++++++++++++++++++++++++++ src/providers/sxyprn.rs | 275 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 570 insertions(+) create mode 100644 src/providers/porn00.rs create mode 100644 src/providers/sxyprn.rs diff --git a/Cargo.toml b/Cargo.toml index e7d3238..85f27c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ wreq-util = "2" percent-encoding = "2.3.2" capitalize = "0.3.4" url = "2.5.4" +base64 = "0.22.1" diff --git a/src/api.rs b/src/api.rs index 2272de8..62611e3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -723,6 +723,40 @@ async fn status(req: HttpRequest) -> Result { nsfw: true, }); + // porn00 + status.add_channel(Channel { + id: "porn00".to_string(), + name: "Porn00".to_string(), + description: "HD Porn".to_string(), + premium: false, + favicon: "https://www.google.com/s2/favicons?sz=64&domain=www.porn00.org".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: "new".to_string(), + title: "New".to_string(), + }, + FilterOption { + id: "popular".to_string(), + title: "Popular".to_string(), + }, + FilterOption { + id: "top-rated".to_string(), + title: "Top Rated".to_string(), + }, + ], + multiSelect: false, + }], + nsfw: true, + }); + //missav status.add_channel(Channel { id: "missav".to_string(), @@ -997,6 +1031,8 @@ pub fn get_provider(channel: &str) -> Option { "hentaimoon" => Some(AnyProvider::Hentaimoon(crate::providers::hentaimoon::HentaimoonProvider::new())), "missav" => Some(AnyProvider::Missav(crate::providers::missav::MissavProvider::new())), "xxthots" => Some(AnyProvider::Xxthots(crate::providers::xxthots::XxthotsProvider::new())), + "sxyprn" => Some(AnyProvider::Sxyprn(crate::providers::sxyprn::SxyprnProvider::new())), + "porn00" => Some(AnyProvider::Porn00(crate::providers::porn00::Porn00Provider::new())), _ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), } } diff --git a/src/main.rs b/src/main.rs index 15b2329..af20fcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ mod videos; type DbPool = r2d2::Pool>; +#[macro_use(c)] +extern crate cute; #[ntex::main] diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 0a6b8ee..f006e13 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -20,6 +20,8 @@ pub mod homoxxx; pub mod hentaimoon; pub mod missav; pub mod xxthots; +pub mod sxyprn; +pub mod porn00; pub trait Provider { async fn get_videos( @@ -52,6 +54,8 @@ pub enum AnyProvider { Hentaimoon(crate::providers::hentaimoon::HentaimoonProvider), Missav(crate::providers::missav::MissavProvider), Xxthots(crate::providers::xxthots::XxthotsProvider), + Sxyprn(crate::providers::sxyprn::SxyprnProvider), + Porn00(crate::providers::porn00::Porn00Provider), } impl Provider for AnyProvider { @@ -142,6 +146,14 @@ impl Provider for AnyProvider { p.get_videos(cache, pool, sort, query, page, per_page, options,) .await } + AnyProvider::Sxyprn(p) => { + p.get_videos(cache, pool, sort, query, page, per_page, options,) + .await + } + AnyProvider::Porn00(p) => { + p.get_videos(cache, pool, sort, query, page, per_page, options,) + .await + } } } } diff --git a/src/providers/porn00.rs b/src/providers/porn00.rs new file mode 100644 index 0000000..42f283c --- /dev/null +++ b/src/providers/porn00.rs @@ -0,0 +1,244 @@ +use crate::util::parse_abbreviated_number; +use crate::DbPool; +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::{ServerOptions, VideoItem}; +use error_chain::error_chain; +use htmlentity::entity::{ICodedDataTrait, decode}; +use std::env; +use std::vec; +use wreq::{Client, Proxy}; +use wreq_util::Emulation; + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(wreq::Error); + } +} + +#[derive(Debug, Clone)] +pub struct Porn00Provider { + url: String, +} +impl Porn00Provider { + pub fn new() -> Self { + Porn00Provider { + url: "www.porn00.org".to_string(), + } + } + async fn get( + &self, + cache: VideoCache, + page: u8, + sort: &str, + options: ServerOptions, + ) -> Result> { + let sort_string = match sort { + "popular" => "/popular-vids", + "top-rated" => "/top-vids", + _ => "/latest-vids/", + }; + + let list_str = match sort { + "popular" => "list_videos_common_videos_list", + "top-rated" => "list_videos_common_videos_list", + _ => "list_videos_most_recent_videos", + }; + + let video_url = format!("{}{}?mode=async^&function=get_block^&block_id={}^&from={}", self.url, sort_string, list_str, page); + let old_items = match cache.get(&video_url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { + println!("Cache hit for URL: {}", video_url); + return Ok(items.clone()); + } else { + items.clone() + } + } + None => { + vec![] + } + }; + + let mut requester = options.requester.clone().unwrap(); + let text = requester.get(&video_url).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, + ) -> Result> { + let search_string = query.to_lowercase().trim().replace(" ", "-"); + let video_url = format!("{}/search/{}/?mode=async&function=get_block&block_id=list_videos_videos_list_search_result&q=a&category_ids=&sort_by=&from_videos={}&from_albums={}&", self.url, search_string, page, page); + // 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 proxy = Proxy::all("http://192.168.0.103:8081").unwrap(); + let client = Client::builder().cert_verification(false).emulation(Emulation::Firefox136).build()?; + + let mut response = client.get(video_url.clone()) + // .proxy(proxy.clone()) + .send().await?; + + if response.status().is_redirection(){ + + response = client.get(self.url.clone() + response.headers()["Location"].to_str().unwrap()) + // .proxy(proxy.clone()) + .send().await?; + } + + if response.status().is_success() { + let text = response.text().await?; + 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) + } 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: video_url.clone(), + maxTimeout: 60000, + }) + .await; + let video_items = match result { + Ok(res) => self.get_video_items_from_html(res.solution.response), + Err(e) => { + println!("Error solving FlareSolverr: {}", e); + return Err("Failed to solve FlareSolverr".into()); + } + }; + 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_video_items_from_html(&self, html: String) -> Vec { + if html.is_empty() { + println!("HTML is empty"); + return vec![]; + } + let mut items: Vec = Vec::new(); + let raw_videos = html.split("
>()[0] + .split("
") + .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 raw_duration = video_segment.split("
").collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; + + let thumb = video_segment.split(">()[1] + .split("data-original=\"").collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(); + + let views_part = video_segment.split("
").collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + let views = parse_abbreviated_number(&views_part).unwrap_or(0) as u32; + + let video_item = VideoItem::new( + id, + title, + video_url.to_string(), + "hentaimoon".to_string(), + thumb, + duration, + ) + .views(views) + ; + items.push(video_item); + } + return items; + } + + +} + +impl Provider for Porn00Provider { + 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,) + .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![] + } + } + } +} diff --git a/src/providers/sxyprn.rs b/src/providers/sxyprn.rs new file mode 100644 index 0000000..983180b --- /dev/null +++ b/src/providers/sxyprn.rs @@ -0,0 +1,275 @@ +use std::vec; +use std::env; +use error_chain::error_chain; +use htmlentity::entity::{decode, ICodedDataTrait}; +use futures::future::join_all; +use wreq::Client; +use wreq::Proxy; +use wreq_util::Emulation; +use crate::db; +use crate::providers::Provider; +use crate::util::cache::VideoCache; +use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr}; +use crate::util::requester; +use crate::videos::ServerOptions; +use crate::videos::{VideoItem}; +use crate::DbPool; +use crate::util::requester::Requester; + +use base64::{engine::general_purpose, Engine as _}; + +/// Extracts digits from a string and sums them. +fn ssut51(arg: &str) -> u32 { + arg.chars() + .filter(|c| c.is_ascii_digit()) + .map(|c| c.to_digit(10).unwrap()) + .sum() +} + +/// Encodes a token: "--" using Base64 URL-safe variant. +fn boo(sum1: u32, sum2: u32, host: &str) -> String { + let raw = format!("{}-{}-{}", sum1, host, sum2); + let encoded = general_purpose::STANDARD.encode(raw); + + // Replace + → -, / → _, = → . + encoded + .replace('+', "-") + .replace('/', "_") + .replace('=', ".") +} + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(wreq::Error); + JsonError(serde_json::Error); + } +} + +#[derive(Debug, Clone)] +pub struct SxyprnProvider { + url: String, +} +impl SxyprnProvider { + pub fn new() -> Self { + SxyprnProvider { + url: "https://sxyprn.com".to_string() + } + } + async fn get(&self, cache:VideoCache, pool:DbPool, page: u8, sort: String, options: ServerOptions) -> Result> { + + // Extract needed fields from options at the start + let language = options.language.clone().unwrap(); + let filter = options.filter.clone().unwrap(); + let mut requester = options.requester.clone().unwrap(); + + let url_str = format!("{}/blog/all/{}.html", self.url, ((page as u32)-1)*20); + + let old_items = match cache.get(&url_str) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { + return Ok(items.clone()); + } + else{ + items.clone() + } + } + None => { + vec![] + } + }; + + let text = requester.get(&url_str).await.unwrap(); + // Pass a reference to options if needed, or reconstruct as needed + let video_items: Vec = self.get_video_items_from_html(text.clone(), pool, requester).await; + if !video_items.is_empty() { + cache.remove(&url_str); + cache.insert(url_str.clone(), video_items.clone()); + } else{ + return Ok(old_items); + } + Ok(video_items) + } + + async fn query(&self, cache: VideoCache, pool:DbPool, page: u8, query: &str, sort: String, options: ServerOptions) -> Result> { + // Extract needed fields from options at the start + let language = options.language.clone().unwrap(); + let filter = options.filter.clone().unwrap(); + let mut requester = options.requester.clone().unwrap(); + let search_string = query.replace(" ", "%20"); + let url_str = format!( + "{}/{}/search/{}?page={}&sort={}", + self.url, language, search_string, page, sort + ); + // Check our Video Cache. If the result is younger than 1 hour, we return it. + let old_items = match cache.get(&url_str) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { + return Ok(items.clone()); + } + else{ + let _ = cache.check().await; + return Ok(items.clone()) + } + } + None => { + vec![] + } + }; + let text = requester.get(&url_str).await.unwrap(); + let video_items: Vec = self.get_video_items_from_html(text.clone(), pool, requester).await; + if !video_items.is_empty() { + cache.remove(&url_str); + cache.insert(url_str.clone(), video_items.clone()); + } else{ + return Ok(old_items); + } + Ok(video_items) + } + + async fn get_video_items_from_html(&self, html: String, pool: DbPool, requester: Requester) -> Vec { + if html.is_empty() { + println!("HTML is empty"); + return vec![]; + } + let raw_videos = html + .split("post_el_small'") + .collect::>()[1..] + .to_vec(); + let mut urls: Vec = 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.to_string().trim()); + } + + let url_str = video_segment.split("data-url='").collect::>()[1] + .split("'") + .collect::>()[0] + .to_string(); + urls.push(url_str.clone()); + break; + + } + let futures = urls.into_iter().map(|el| self.get_video_item(el.clone(), pool.clone(), requester.clone())); + let results: Vec> = join_all(futures).await; + let video_items: Vec = results + .into_iter() + .filter_map(Result::ok) + .collect(); + + return video_items; + } + + async fn get_video_item(&self, url_str: String, pool: DbPool, mut requester: Requester) -> Result { + let mut conn = pool.get().expect("couldn't get db connection from pool"); + let db_result = db::get_video(&mut conn,url_str.clone()); + match db_result { + Ok(Some(entry)) => { + let video_item: VideoItem = serde_json::from_str(entry.as_str()).unwrap(); + return Ok(video_item) + } + Ok(None) => { + } + Err(e) => { + println!("Error fetching video from database: {}", e); + } + } + drop(conn); + let vid = requester.get(&url_str).await.unwrap().to_string(); + let mut title = vid.split("").collect::<Vec<&str>>()[1] + .split(" #") + .collect::<Vec<&str>>()[0].trim() + .to_string(); + title = decode(title.as_bytes()).to_string().unwrap_or(title); + let thumb = format!("https:{}", vid.split("<meta property='og:image' content='").collect::<Vec<&str>>()[1] + .split("\"") + .collect::<Vec<&str>>()[0] + .to_string()); + + let raw_duration = vid.split("duration:<b>").collect::<Vec<&str>>()[1] + .split("</b>") + .collect::<Vec<&str>>()[0] + .to_string(); + let duration = raw_duration.parse::<u32>().unwrap_or(0); + + let id = url_str.split("/").collect::<Vec<&str>>().last().unwrap().replace(".html", "") + .to_string(); + let mut tags = vec![]; + if vid.split("splitter_block_header").collect::<Vec<&str>>()[0].contains("hash_link"){ + for tag_snippet in vid.split("splitter_block_header").collect::<Vec<&str>>()[0].split("hash_link").collect::<Vec<&str>>()[1..].to_vec(){ + let tag = tag_snippet.split("<").collect::<Vec<&str>>()[0].trim() + .to_string(); + if !tag.is_empty(){ + tags.push(tag.replace("#", "")); + } + } + } + let video_url_pre_str = vid.split("data-vnfo").collect::<Vec<&str>>()[1] + .split("\":\"").collect::<Vec<&str>>()[1] + .split("\"").collect::<Vec<&str>>()[0] + .replace("\\", "") + .to_string(); + println!("Video URL pre str: {}", video_url_pre_str); + let video_request = requester.get(&url_str).await.unwrap(); + let mut video_url_parts = vid.split("m3u8").collect::<Vec<&str>>()[1] + .split("https").collect::<Vec<&str>>()[0] + .split("|").collect::<Vec<&str>>(); + video_url_parts.reverse(); + let video_url = format!("https://{}.{}/{}-{}-{}-{}-{}/playlist.m3u8", + video_url_parts[1], + video_url_parts[2], + video_url_parts[3], + video_url_parts[4], + video_url_parts[5], + video_url_parts[6], + video_url_parts[7] + ); + let video_item = VideoItem::new( + id, + title, + video_url.clone(), + "sxyprn".to_string(), + thumb, + duration, + ) + .tags(tags) + ; + + let mut conn = pool.get().expect("couldn't get db connection from pool"); + let insert_result = db::insert_video(&mut conn, &url_str, &serde_json::to_string(&video_item)?); + match insert_result{ + Ok(_) => (), + Err(e) => {println!("{:?}", e); } + } + drop(conn); + + return Ok(video_item); + } +} + +impl Provider for SxyprnProvider { + async fn get_videos( + &self, + cache: VideoCache, + pool: DbPool, + sort: String, + query: Option<String>, + page: String, + per_page: String, + options: ServerOptions, + ) -> Vec<VideoItem> { + let _ = per_page; + let videos: std::result::Result<Vec<VideoItem>, Error> = match query { + Some(q) => self.query(cache, pool, page.parse::<u8>().unwrap_or(1), &q, sort, options).await, + None => self.get(cache, pool, page.parse::<u8>().unwrap_or(1), sort, options).await, + }; + match videos { + Ok(v) => v, + Err(e) => { + println!("Error fetching videos: {}", e); + vec![] + } + } + } +}