diff --git a/src/api.rs b/src/api.rs index 290a3b5..8a1477a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -6,6 +6,7 @@ use tokio::{task}; use crate::providers::hanime::HanimeProvider; use crate::providers::perverzija::PerverzijaProvider; +use crate::providers::pornhub::PornhubProvider; use crate::providers::spankbang::SpankbangProvider; use crate::util::cache::VideoCache; use crate::{providers::*, status::*, videos::*, DbPool}; @@ -115,7 +116,17 @@ async fn status(req: HttpRequest) -> Result { .unwrap_or_default() .to_string(); let mut status = Status::new(); - + 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, + }); if clientversion >= ClientVersion::new(22,97,"22a".to_string()){ //add perverzija status.add_channel(Channel { @@ -370,6 +381,7 @@ pub fn get_provider(channel: &str) -> Option { "perverzija" => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), "hanime" => Some(AnyProvider::Hanime(HanimeProvider::new())), "spankbang" => Some(AnyProvider::Spankbang(SpankbangProvider::new())), + "pornhub" => Some(AnyProvider::Pornhub(PornhubProvider::new())), _ => Some(AnyProvider::Perverzija(PerverzijaProvider::new())), } } \ No newline at end of file diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 890193c..c7a4886 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1,8 +1,9 @@ -use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider, spankbang::SpankbangProvider}, util::cache::VideoCache, videos::VideoItem, DbPool}; +use crate::{providers::{hanime::HanimeProvider, perverzija::PerverzijaProvider, pornhub::PornhubProvider, spankbang::SpankbangProvider}, util::cache::VideoCache, videos::VideoItem, DbPool}; pub mod perverzija; pub mod hanime; pub mod spankbang; +pub mod pornhub; 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; @@ -13,6 +14,7 @@ pub enum AnyProvider { Perverzija(PerverzijaProvider), Hanime(HanimeProvider), Spankbang(SpankbangProvider), + Pornhub(PornhubProvider), } 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 { @@ -24,6 +26,7 @@ impl Provider for AnyProvider { AnyProvider::Perverzija(p) => p.get_videos(cache.clone(), pool.clone(), channel.clone(), sort.clone(), query.clone(), page.clone(), per_page.clone(), featured.clone()).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, + AnyProvider::Pornhub(p) => p.get_videos(cache, pool, channel, sort, query, page, per_page, featured).await, } } } diff --git a/src/providers/pornhub.rs b/src/providers/pornhub.rs new file mode 100644 index 0000000..f3e4afd --- /dev/null +++ b/src/providers/pornhub.rs @@ -0,0 +1,252 @@ +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::{VideoItem}; +use error_chain::error_chain; +use htmlentity::entity::{ICodedDataTrait, decode}; +use std::env; +use std::vec; +use wreq::Client; +use wreq_util::Emulation; + +error_chain! { + foreign_links { + Io(std::io::Error); + HttpRequest(wreq::Error); + } +} + +#[derive(Debug, Clone)] +pub struct PornhubProvider { + url: String, +} +impl PornhubProvider { + pub fn new() -> Self { + PornhubProvider { + url: "https://pornhub.com".to_string(), + } + } + async fn get( + &self, + cache: VideoCache, + page: u8, + ) -> Result> { + let mut url = format!("{}/video?page={}", self.url, page); + if page == 1 { + url = format!("{}/video", self.url); + } + + 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 = Client::builder().emulation(Emulation::Firefox136).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> { + let search_string = query.replace(" ", "+"); + let mut url = format!("{}/video?search={}&page={}", self.url, search_string, page); + if page == 1 { + url = format!("{}/video?search={}", self.url, search_string); + } + + // Check our Video Cache. If the result is younger than 1 hour, we return it. + let old_items = match cache.get(&url) { + 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 client = Client::builder().emulation(Emulation::Firefox136).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) => 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("
    >()[1]; + let raw_videos = video_listing_content + .split("class=\"pcVideoListItem ") + .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 mut title = vid[16].split("title=\"").collect::>()[1] + .split("") + .collect::>()[0] + .to_string(); + // html decode + title = decode(title.as_bytes()).to_string().unwrap_or(title); + let url = format!("{}{}", self.url, vid[16].split("href=\"").collect::>()[1] + .split("\"") + .collect::>()[0]); + let id = vid[2].split("data-video-id=\"").collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(); + let raw_duration = vid[29].split("").collect::>()[1] + .split("<") + .collect::>()[0] + .to_string(); + let duration = parse_time_to_seconds(&raw_duration).unwrap_or(0) as u32; + + let thumb = vid[20].split("src=\"").collect::>()[1] + .split("\"") + .collect::>()[0] + .to_string(); + let video_item = VideoItem::new( + id, + title, + url.to_string(), + "perverzija".to_string(), + thumb, + duration, + ); + items.push(video_item); + } + return items; + } + + +} + +impl Provider for PornhubProvider { + 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 _ = sort; + let _ = featured; // Ignored in this implementation + let _ = pool; // Ignored in this implementation + 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)) + .await + } + }; + match videos { + Ok(v) => v, + Err(e) => { + println!("Error fetching videos: {}", e); + vec![] + } + } + } +} diff --git a/src/providers/spankbang.rs b/src/providers/spankbang.rs index 88b4f25..b48efb8 100644 --- a/src/providers/spankbang.rs +++ b/src/providers/spankbang.rs @@ -3,7 +3,6 @@ use std::env; use error_chain::error_chain; use futures::future::join_all; use htmlentity::entity::{decode, ICodedDataTrait}; -use wreq::Proxy; use crate::db; use crate::providers::Provider; use crate::util::cache::VideoCache;