diff --git a/src/api.rs b/src/api.rs index b548635..186df41 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,6 +4,7 @@ use ntex::web::HttpRequest; use crate::providers::hanime::HanimeProvider; use crate::providers::perverzija::PerverzijaProvider; +use crate::providers::spankbang::SpankbangProvider; use crate::util::cache::VideoCache; use crate::{providers::*, status::*, videos::*, DbPool}; @@ -28,89 +29,6 @@ async fn status(req: HttpRequest) -> Result { .unwrap_or_default() .to_string(); let mut status = Status::new(); - - // You can now use `method`, `host`, and `port` as needed - - // status.add_channel(Channel { - // id: "all".to_string(), - // name: "SpaceMoehre's Hottub".to_string(), - // favicon: format!("http://{}/static/favicon.ico", host).to_string(), - // premium: false, - // description: "Work in Progress".to_string(), - // status: "active".to_string(), - // categories: vec![], - // options: vec![ - // ChannelOption { - // id: "channels".to_string(), - // title: "Sites".to_string(), - // description: "Websites included in search results.".to_string(), - // systemImage: "network".to_string(), - // colorName: "purple".to_string(), - // options: vec![ - // FilterOption { - // id: "perverzija".to_string(), - // title: "Perverzija".to_string(), - // }, - // ], - // multiSelect: true, - // }, - // 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: "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, - // }, - // 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, - // }, - // ], - // nsfw: true, - // }); status.add_channel(Channel { id: "perverzija".to_string(), name: "Perverzija".to_string(), @@ -239,6 +157,39 @@ async fn status(req: HttpRequest) -> Result { ], 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)) } @@ -293,6 +244,7 @@ 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())), _ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f7f4c49..df5a133 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod videos; type DbPool = r2d2::Pool>; +static USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15"; #[ntex::main] async fn main() -> std::io::Result<()> { // std::env::set_var("RUST_BACKTRACE", "1"); diff --git a/src/providers/all.rs b/src/providers/all.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/providers/hanime.rs b/src/providers/hanime.rs index 4bc2285..473a877 100644 --- a/src/providers/hanime.rs +++ b/src/providers/hanime.rs @@ -8,7 +8,8 @@ use crate::db; use crate::providers::Provider; use crate::util::cache::VideoCache; use crate::videos::{self, VideoItem}; -use crate::DbPool; // Make sure Provider trait is imported +use crate::DbPool; +use crate::USER_AGENT; // Make sure Provider trait is imported error_chain! { foreign_links { @@ -153,12 +154,12 @@ impl HanimeProvider { let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .proxy(Proxy::https(burp_url).unwrap()) .danger_accept_invalid_certs(true) .build()?, Err(_) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .danger_accept_invalid_certs(true) .build()?, }; @@ -229,12 +230,12 @@ impl HanimeProvider { let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .proxy(Proxy::https(burp_url).unwrap()) .danger_accept_invalid_certs(true) .build()?, Err(_) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .danger_accept_invalid_certs(true) .build()?, }; diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 6091a8d..9d3ec77 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1,7 +1,8 @@ -use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider}, util::cache::VideoCache, videos::VideoItem, DbPool}; +use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider, spankbang::SpankbangProvider}, util::cache::VideoCache, videos::VideoItem, DbPool}; pub mod perverzija; pub mod hanime; +pub mod spankbang; pub trait Provider{ async fn get_videos(&self, cache: VideoCache, pool: DbPool, channel: String, sort: String, query: Option, page: String, per_page: String, featured: String) -> Vec; @@ -10,12 +11,14 @@ pub trait Provider{ pub enum AnyProvider { Perverzija(PerverzijaProvider), Hanime(HanimeProvider), + Spankbang(SpankbangProvider), } impl Provider for AnyProvider { async fn get_videos(&self, cache: VideoCache, pool:DbPool, channel: String, sort: String, query: Option, page: String, per_page: String, featured: String) -> Vec { match self { AnyProvider::Perverzija(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await, AnyProvider::Hanime(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await, + AnyProvider::Spankbang(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await, } } } diff --git a/src/providers/perverzija.rs b/src/providers/perverzija.rs index 884d5d4..f43497d 100644 --- a/src/providers/perverzija.rs +++ b/src/providers/perverzija.rs @@ -11,7 +11,8 @@ use crate::util::cache::VideoCache; use crate::util::flaresolverr::{FlareSolverrRequest, Flaresolverr}; use crate::util::time::parse_time_to_seconds; use crate::videos::{self, VideoEmbed, VideoItem}; -use crate::DbPool; // Make sure Provider trait is imported +use crate::DbPool; +use crate::USER_AGENT; // Make sure Provider trait is imported error_chain! { foreign_links { @@ -65,12 +66,12 @@ impl PerverzijaProvider { let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .proxy(Proxy::https(burp_url).unwrap()) .danger_accept_invalid_certs(true) .build()?, Err(_) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .danger_accept_invalid_certs(true) .build()?, }; @@ -144,12 +145,12 @@ impl PerverzijaProvider { let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .proxy(Proxy::https(burp_url).unwrap()) .danger_accept_invalid_certs(true) .build()?, Err(_) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .danger_accept_invalid_certs(true) .build()?, }; @@ -387,12 +388,12 @@ impl PerverzijaProvider { let client = match env::var("BURP_URL").as_deref() { Ok(burp_url) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .proxy(Proxy::https(burp_url).unwrap()) .danger_accept_invalid_certs(true) .build()?, Err(_) => reqwest::Client::builder() - .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15") + .user_agent(USER_AGENT) .danger_accept_invalid_certs(true) .build()?, }; diff --git a/src/providers/spankbang.rs b/src/providers/spankbang.rs new file mode 100644 index 0000000..c6a4151 --- /dev/null +++ b/src/providers/spankbang.rs @@ -0,0 +1,271 @@ +use std::fs; +use std::vec; +use std::env; +use std::io::Write; +use error_chain::error_chain; +use htmlentity::entity::{decode, ICodedDataTrait}; +use reqwest::{Proxy}; +use futures::future::join_all; + +use crate::db; +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, VideoEmbed, VideoItem}; +use crate::DbPool; +use crate::USER_AGENT; // Make sure Provider trait is imported + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(reqwest::Error); + } +} + +pub struct SpankbangProvider { + url: String, +} +impl SpankbangProvider { + pub fn new() -> Self { + SpankbangProvider { + url: "https://spankbang.com/".to_string(), + } + } + async fn get(&self, cache:VideoCache, page: u8, sort: String) -> Result> { + let mut url = format!("{}{}/{}/", self.url, sort, page); + + let old_items = match cache.get(&url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { + println!("Cache hit for URL: {}", url); + return Ok(items.clone()); + } + else{ + items.clone() + } + } + None => { + vec![] + } + }; + + + let client = match env::var("BURP_URL").as_deref() { + Ok(burp_url) => + reqwest::Client::builder() + .user_agent(USER_AGENT) + .proxy(Proxy::https(burp_url).unwrap()) + .danger_accept_invalid_certs(true) + .build()?, + Err(_) => reqwest::Client::builder() + .user_agent(USER_AGENT) + .danger_accept_invalid_certs(true) + .build()?, + }; + + let response = client.get(url.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(&url); + cache.insert(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: url.clone(), + maxTimeout: 60000, + }) + .await; + 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()); + } + }; + if !video_items.is_empty() { + cache.remove(&url); + cache.insert(url.clone(), video_items.clone()); + } else { + return Ok(old_items); + } + Ok(video_items) + } + } + + async fn query(&self, cache: VideoCache, page: u8, query: &str,) -> Result> { + println!("query"); + let url = format!("{}s/{}/{}/", self.url, query.replace(" ", "+"), page); + + let old_items = match cache.get(&url) { + Some((time, items)) => { + if time.elapsed().unwrap_or_default().as_secs() < 60 * 60 { + println!("Cache hit for URL: {}", url); + return Ok(items.clone()); + } + else{ + items.clone() + } + } + None => { + vec![] + } + }; + + + let client = match env::var("BURP_URL").as_deref() { + Ok(burp_url) => + reqwest::Client::builder() + .user_agent(USER_AGENT) + .proxy(Proxy::https(burp_url).unwrap()) + .danger_accept_invalid_certs(true) + .build()?, + Err(_) => reqwest::Client::builder() + .user_agent(USER_AGENT) + .danger_accept_invalid_certs(true) + .build()?, + }; + + let response = client.get(url.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(&url); + cache.insert(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: url.clone(), + maxTimeout: 60000, + }) + .await; + 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()); + } + }; + if !video_items.is_empty() { + cache.remove(&url); + cache.insert(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 video_listing_content = html.split("class=\"video-list ").collect::>()[1]; + let raw_videos = video_listing_content + .split("class=\"video-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 {}: {}\n\n", index, line); + //} + let mut title = vid[4].split("title=\"").collect::>()[1].split("\"").collect::>()[0].to_string(); + title = decode(title.as_bytes()).to_string().unwrap_or(title); + let thumb = vid[14].split("data-src=\"").collect::>()[1].split("\"").collect::>()[0].to_string(); + let preview = vid[16].split("data-preview=\"").collect::>()[1].split("\"").collect::>()[0].to_string(); + let duration_str = vid.iter().find(|s| s.contains("")).unwrap().split("").collect::>()[1].split("m<").collect::>()[0]; + let duration: u32 = duration_str.parse::().unwrap_or(0) * 60; + let view_and_rating_str: Vec<&str> = vid.iter().copied().filter(|s| s.contains("")).collect(); + let views_str = view_and_rating_str[0]; + let views = views_str.parse::().unwrap_or(0) * 1000; + let rate_str = view_and_rating_str[1]; + let rating = rate_str.parse::().unwrap_or(0.0); + let url = self.url.clone() + vid.iter().find(|s| s.contains(">()[1].split("\"").collect::>()[0]; + let id = url.split("/").collect::>()[0].to_string(); + + let quality_str = match vid[25].contains("<"){ + true => vid[25].split(">").collect::>()[1].split("<").collect::>()[0], + false => "SD", + }; + let quality = match quality_str{ + "HD" => "1080", + "4k" => "2160", + "SD" => "720", + _ => "1080", + }; + + + let mut format = + videos::VideoFormat::new(url.clone(), quality.to_string(), "mp4".to_string()); + format.add_http_header("User-Agent".to_string(), USER_AGENT.to_string()); + + let mut video_item = VideoItem::new(id, title, url.clone().to_string(), "spankbang".to_string(), thumb, duration) + .views(views) + .rating(rating) + .preview(preview); + items.push(video_item); + } + return items; + } + + async fn get_video_items_from_html_query(&self, html: String) -> Vec { + let mut items: Vec = Vec::new(); + return items; + } + +} + +impl Provider for SpankbangProvider { + async fn get_videos( + &self, + cache: VideoCache, + pool: DbPool, + _channel: String, + sort: String, + query: Option, + page: String, + per_page: String, + featured: String, + ) -> Vec { + let _ = per_page; + let _ = featured; + 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).await, + }; + match videos { + Ok(v) => v, + Err(e) => { + println!("Error fetching videos: {}", e); + vec![] + } + } + } +} diff --git a/src/videos.rs b/src/videos.rs index ee9c893..4aa2b20 100644 --- a/src/videos.rs +++ b/src/videos.rs @@ -64,6 +64,7 @@ pub struct VideoItem { pub uploadedAt: Option, // 1741142954 pub formats: Option>, // Additional HTTP headers if needed pub embed: Option, // Optional embed information + pub preview: Option } #[allow(dead_code)] impl VideoItem { @@ -91,6 +92,7 @@ impl VideoItem { uploadedAt: None, formats: None, // Placeholder for formats embed: None, // Placeholder for embed information + preview: None } } pub fn tags(mut self, tags: Vec) -> Self { @@ -125,10 +127,21 @@ impl VideoItem { self.formats = Some(formats); self } + pub fn add_format(mut self, format: VideoFormat){ + if let Some(formats) = self.formats.as_mut() { + formats.push(format); + } else { + self.formats = Some(vec![format]); + } + } pub fn embed(mut self, embed: VideoEmbed) -> Self { self.embed = Some(embed); self } + pub fn preview(mut self, preview: String) -> Self { + self.preview = Some(preview); + self + } } #[derive(serde::Serialize, Debug, Clone)]